Implement capability generics, checked package specializations and CRUD scaffolding
Add kinded parameters, capability bounds, Self, aliases and closed application identities. Check generic implementations universally and build candidate-specific codecs and descriptors from immutable schemas. Preserve lexical aliases and exact dispatch identities in package and host bindings. Add an imperative CRUD+index domain scaffold with explicit soft-deletion semantics, source/codegen regression coverage, installed CLI tests and an authoring guide. Existing Web Studio opaque props and class-level create-menu migration are separate from the implemented language core.
This commit is contained in:
+52
-10
@@ -27,6 +27,7 @@ workspaceItem
|
||||
| sharedAttachmentDecl
|
||||
| conformanceDecl
|
||||
| constructorBindingDecl
|
||||
| typeAliasDecl
|
||||
;
|
||||
|
||||
resourceImportDecl
|
||||
@@ -46,6 +47,7 @@ resourcePreamble
|
||||
: resourceImportDecl
|
||||
| externalAtomDecl
|
||||
| externalInterfaceDecl
|
||||
| typeAliasDecl
|
||||
;
|
||||
|
||||
atomDecl
|
||||
@@ -53,10 +55,39 @@ atomDecl
|
||||
;
|
||||
|
||||
interfaceResourceDecl
|
||||
: resourcePreamble* INTERFACE identifier ID stringLiteral REVISION stringLiteral
|
||||
: resourcePreamble* INTERFACE identifier typeParameters? ID stringLiteral REVISION stringLiteral
|
||||
(REQUIRES interfaceType (COMMA interfaceType)*)?
|
||||
LBRACE interfaceMember* RBRACE
|
||||
;
|
||||
|
||||
typeParameters
|
||||
: LT typeParameter (COMMA typeParameter)* GT
|
||||
;
|
||||
|
||||
typeParameter
|
||||
: VALUE identifier (COLON STORABLE)?
|
||||
| OBJECT identifier (IMPLEMENTS interfaceType (AMP interfaceType)*)?
|
||||
;
|
||||
|
||||
interfaceType
|
||||
: identifier typeArguments?
|
||||
;
|
||||
|
||||
typeArguments
|
||||
: LT typeArgument (COMMA typeArgument)* GT
|
||||
;
|
||||
|
||||
typeArgument
|
||||
: ATOM identifier
|
||||
| INTERFACE interfaceType
|
||||
| OBJECT identifier
|
||||
| valueType
|
||||
;
|
||||
|
||||
typeAliasDecl
|
||||
: TYPE identifier typeParameters? EQUAL valueType SEMI
|
||||
;
|
||||
|
||||
interfaceMember
|
||||
: valueMember
|
||||
| relationshipMember
|
||||
@@ -93,7 +124,8 @@ relationshipOperation
|
||||
|
||||
targetConstraint
|
||||
: ATOM identifier
|
||||
| INTERFACE identifier
|
||||
| INTERFACE identifier typeArguments?
|
||||
| OBJECT identifier
|
||||
;
|
||||
|
||||
packageResourceDecl
|
||||
@@ -109,12 +141,12 @@ packageExport
|
||||
;
|
||||
|
||||
packageOperationExport
|
||||
: OPERATION identifier ID stringLiteral COLON valueType ARROW valueType
|
||||
: OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
|
||||
MODE operationMode eventClause? RECEIVER receiverRequirement dependencyBlock? SEMI
|
||||
;
|
||||
|
||||
packageFunctionExport
|
||||
: FUNCTION identifier ID stringLiteral COLON valueType ARROW valueType
|
||||
: FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
|
||||
dependencyBlock? SEMI
|
||||
;
|
||||
|
||||
@@ -138,7 +170,8 @@ operationMode
|
||||
receiverRequirement
|
||||
: ANY
|
||||
| ATOM identifier
|
||||
| INTERFACES LBRACK identifierList? RBRACK
|
||||
| OBJECT identifier
|
||||
| INTERFACES LBRACK (interfaceType (COMMA interfaceType)*)? RBRACK
|
||||
;
|
||||
|
||||
identifierList
|
||||
@@ -152,7 +185,7 @@ dependencyBlock
|
||||
dependencyPort
|
||||
: STATE identifier ID stringLiteral COLON valueType primitiveList SEMI
|
||||
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI
|
||||
| INTERFACE identifier ID stringLiteral COLON identifier SEMI
|
||||
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? SEMI
|
||||
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
|
||||
;
|
||||
|
||||
@@ -199,7 +232,7 @@ edgeEndpoint
|
||||
;
|
||||
|
||||
conformanceDecl
|
||||
: CONFORM identifier AS identifier (ID stringLiteral)? (SEMANTIC_MAJOR INTEGER)?
|
||||
: CONFORM identifier AS identifier typeArguments? (ID stringLiteral)? (SEMANTIC_MAJOR INTEGER)?
|
||||
LBRACE conformanceItem* RBRACE
|
||||
;
|
||||
|
||||
@@ -238,7 +271,7 @@ operationName
|
||||
operationProvider
|
||||
: STATE identifier DOT statePrimitive
|
||||
| EDGE identifier DOT identifier DOT edgePrimitive
|
||||
| PACKAGE identifier DOT identifier dependencyBindingBlock?
|
||||
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock?
|
||||
;
|
||||
|
||||
statePrimitive
|
||||
@@ -263,7 +296,7 @@ dependencyBindingBlock
|
||||
dependencyBinding
|
||||
: identifier TO STATE identifier (VIA EDGE identifier DOT identifier)? SEMI
|
||||
| identifier TO EDGE identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
|
||||
| identifier TO INTERFACE identifier (VIA EDGE identifier DOT identifier)? SEMI
|
||||
| identifier TO INTERFACE identifier typeArguments? (VIA EDGE identifier DOT identifier)? SEMI
|
||||
| identifier TO CONSTRUCTOR identifier SEMI
|
||||
;
|
||||
|
||||
@@ -277,10 +310,12 @@ valueType
|
||||
| WATCH_HANDLE
|
||||
| MESSAGE stringLiteral
|
||||
| ATOM_REF LT identifier GT
|
||||
| INTERFACE_REF LT identifier GT
|
||||
| INTERFACE_REF LT identifier typeArguments? GT
|
||||
| REF LT identifier GT
|
||||
| OPTIONAL LT valueType GT
|
||||
| LIST LT valueType GT
|
||||
| RECORD LBRACE recordField* RBRACE
|
||||
| identifier typeArguments?
|
||||
;
|
||||
|
||||
recordField
|
||||
@@ -338,6 +373,11 @@ stringLiteral
|
||||
;
|
||||
|
||||
WORKSPACE: 'workspace';
|
||||
TYPE: 'type';
|
||||
OBJECT: 'object';
|
||||
STORABLE: 'storable';
|
||||
IMPLEMENTS: 'implements';
|
||||
REF: 'ref';
|
||||
FRAGMENT: 'fragment';
|
||||
IMPORT: 'import';
|
||||
EXTERNAL: 'external';
|
||||
@@ -441,6 +481,8 @@ LPAREN: '(';
|
||||
RPAREN: ')';
|
||||
LT: '<';
|
||||
GT: '>';
|
||||
AMP: '&';
|
||||
EQUAL: '=';
|
||||
|
||||
INTEGER: '-'? [0-9]+;
|
||||
JSON_NUMBER: '-'? ('0' | [1-9] [0-9]*) ('.' [0-9]+)? ([eE] [+-]? [0-9]+)?;
|
||||
|
||||
@@ -70,7 +70,7 @@ let
|
||||
${protocol}/bin/quixos-workspace-compile --root ${node.directory} \
|
||||
--source-root-commit ${pkgs.lib.escapeShellArg node.commit} \
|
||||
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots} \
|
||||
--graph-out "$out/graph.json" > "$out/candidate.json"
|
||||
--graph-out "$out/graph.json" --schemas-out "$out/package-schemas.json" > "$out/candidate.json"
|
||||
''
|
||||
else
|
||||
''
|
||||
@@ -95,7 +95,15 @@ let
|
||||
package.quixosPackages.${system}.checkedServer
|
||||
or (throw "Package ${node.repository} lacks checkedServer; use the supported package scaffold.");
|
||||
artifact = checked {
|
||||
schema = "${compiled}/bindings.json";
|
||||
schema =
|
||||
if kind == "workspace" then
|
||||
pkgs.writeText "package-specialization-schema.json" (
|
||||
builtins.toJSON
|
||||
(builtins.fromJSON (builtins.readFile "${contract}/package-schemas.json"))
|
||||
.${candidate.revision.revisionId}
|
||||
)
|
||||
else
|
||||
"${compiled}/bindings.json";
|
||||
generator = protocol;
|
||||
packageRevisionId = candidate.revision.revisionId;
|
||||
};
|
||||
|
||||
+40
-2
@@ -2,7 +2,13 @@ import type { InterfaceRevision, ValueType } from "../capability-model/types.js"
|
||||
|
||||
/** Host clients have no package receiver, but must use the same checked
|
||||
* interface signatures and argument framing as generated package ports. */
|
||||
export const generateClientContracts = (interfaces: InterfaceRevision[], messages: Record<string, string>) => {
|
||||
export const generateClientContracts = (
|
||||
interfaces: InterfaceRevision[],
|
||||
messages: Record<string, string>,
|
||||
qualified = false,
|
||||
) => {
|
||||
if (interfaces.some((entry) => entry.template))
|
||||
throw new Error("Host contracts require closed interface applications, not generic definitions");
|
||||
const type = (value: ValueType): string => {
|
||||
switch (value.kind) {
|
||||
case "builtin":
|
||||
@@ -19,7 +25,9 @@ export const generateClientContracts = (interfaces: InterfaceRevision[], message
|
||||
uint64: "bigint",
|
||||
}[value.name];
|
||||
case "object-ref":
|
||||
return `{readonly $quixosRef: string}`;
|
||||
return qualified
|
||||
? `CapabilityReference<${JSON.stringify(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`
|
||||
: `{readonly $quixosRef: string}`;
|
||||
case "optional":
|
||||
return `(${type(value.value)} | null)`;
|
||||
case "list":
|
||||
@@ -42,6 +50,30 @@ export const generateClientContracts = (interfaces: InterfaceRevision[], message
|
||||
.map((operation) => ({ ...operation, interfaceRevisionId: iface.revisionId })),
|
||||
),
|
||||
);
|
||||
if (qualified) {
|
||||
const contracts = interfaces.map((iface) => {
|
||||
const members = iface.members.flatMap((member) =>
|
||||
member.operations
|
||||
.filter((op) => op.mode === "call")
|
||||
.map((op) => ` ${JSON.stringify(op.id)}: {input: ${type(op.inputType)}; output: ${type(op.outputType)}};`),
|
||||
);
|
||||
return `${JSON.stringify(iface.revisionId)}: {\n${members.join("\n")}\n}`;
|
||||
});
|
||||
if (new Set(interfaces.map((iface) => iface.revisionId)).size !== interfaces.length)
|
||||
throw new Error("Duplicate closed interface identity");
|
||||
return (
|
||||
`// Generated closed capability contracts. Dispatch by interface AND operation.\n` +
|
||||
`declare const referenceType: unique symbol;\nexport type CapabilityReference<T extends string> = {readonly $quixosRef: string; readonly [referenceType]: T};\n` +
|
||||
`export type CapabilityContracts = {${contracts.join(";\n")}};\n` +
|
||||
`export type CapabilityInput<I extends keyof CapabilityContracts, O extends keyof CapabilityContracts[I]> = CapabilityContracts[I][O] extends {input: infer T} ? T : never;\n` +
|
||||
`export type CapabilityOutput<I extends keyof CapabilityContracts, O extends keyof CapabilityContracts[I]> = CapabilityContracts[I][O] extends {output: infer T} ? T : never;\n` +
|
||||
`export const capabilityApplications = ${JSON.stringify(Object.fromEntries(interfaces.map((iface) => [iface.revisionId, { definitionId: iface.application?.definitionId ?? iface.revisionId, arguments: iface.application?.arguments ?? [], ...(iface.application?.self ? { self: iface.application.self } : {}) }])))} as const;\n`
|
||||
);
|
||||
}
|
||||
if (new Set(operations.map((entry) => entry.id)).size !== operations.length)
|
||||
throw new Error(
|
||||
"Host operation IDs are ambiguous across interfaces; select a closed interface application explicitly",
|
||||
);
|
||||
return (
|
||||
`// Generated from checked QX interfaces. Regenerate with scripts/generate-platform-contracts.mjs.\n` +
|
||||
`export type PlatformInputs = {\n${operations.map((operation) => ` ${JSON.stringify(operation.id)}: ${type(operation.inputType)};`).join("\n")}\n};\n` +
|
||||
@@ -65,3 +97,9 @@ export const generateClientContracts = (interfaces: InterfaceRevision[], message
|
||||
)} as const;\n`
|
||||
);
|
||||
};
|
||||
|
||||
/** New consumers use exact closed interface identities; operation IDs alone are not unique. */
|
||||
export const generateAppliedClientContracts = (
|
||||
interfaces: InterfaceRevision[],
|
||||
messages: Record<string, string> = {},
|
||||
) => generateClientContracts(interfaces, messages, true);
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { GenericPackageExport, GenericDependencyPort } from "../capability-model/generic-packages.js";
|
||||
import type { InterfaceRevision, ValueType } from "../capability-model/types.js";
|
||||
import type {
|
||||
ValueTypeExpression,
|
||||
ObjectTypeExpression,
|
||||
TypeArgumentExpression,
|
||||
TypeParameter,
|
||||
ValueAliasDefinition,
|
||||
} from "../capability-model/generics.js";
|
||||
|
||||
/** Universal source contracts. Only closed exports get executable codecs. */
|
||||
export const genericImplementationType = (
|
||||
definition: GenericPackageExport,
|
||||
interfaces: InterfaceRevision[],
|
||||
concrete: (type: ValueType) => string,
|
||||
): string => {
|
||||
const names = new Map(definition.parameters.map((parameter, index) => [parameter.id, `T${index}`]));
|
||||
type Scope = { arguments: Map<string, string>; aliases: ValueAliasDefinition[] };
|
||||
const rootScope = (): Scope => ({ arguments: new Map(), aliases: definition.aliases });
|
||||
const object = (entries: [string, string][]) =>
|
||||
`{${entries.map(([key, value]) => `${JSON.stringify(key)}:${value}`).join(";")}}`;
|
||||
const target = (type: ObjectTypeExpression, scope: Scope): string => {
|
||||
if (type.kind === "parameter") {
|
||||
const bound = scope.arguments.get(type.parameterId);
|
||||
if (bound) return bound;
|
||||
const name = names.get(type.parameterId);
|
||||
if (!name) throw new Error(`Unbound object parameter ${type.parameterId}`);
|
||||
return name;
|
||||
}
|
||||
if (type.kind === "atom") return JSON.stringify(`atom:${type.atomId}`);
|
||||
if (type.kind === "interface") return JSON.stringify(`interface:${type.interfaceRevisionId}`);
|
||||
if (type.kind === "application")
|
||||
return `QxApplied<${JSON.stringify(type.application.definitionId)}, [${type.application.arguments.map((arg) => argument(arg, scope)).join(",")}]>`;
|
||||
throw new Error("Generic package Self must be expressed as an explicit object parameter");
|
||||
};
|
||||
const argument = (arg: TypeArgumentExpression, scope: Scope): string =>
|
||||
arg.kind === "value" ? value(arg.type, scope) : target(arg.target, scope);
|
||||
const bind = (parameters: TypeParameter[], args: TypeArgumentExpression[], scope: Scope): Scope => {
|
||||
if (parameters.length !== args.length) throw new Error("Wrong generic arity during code generation");
|
||||
const result = new Map(scope.arguments);
|
||||
// Render arguments in their original lexical scope before introducing binders.
|
||||
const rendered = args.map((arg) => argument(arg, scope));
|
||||
parameters.forEach((parameter, index) => result.set(parameter.id, rendered[index]));
|
||||
return { ...scope, arguments: result };
|
||||
};
|
||||
const value = (type: ValueTypeExpression, scope: Scope, depth = 0): string => {
|
||||
if (depth > 128) throw new Error("Generic type expansion exceeds depth limit");
|
||||
switch (type.kind) {
|
||||
case "parameter": {
|
||||
const bound = scope.arguments.get(type.parameterId);
|
||||
if (bound) return bound;
|
||||
const name = names.get(type.parameterId);
|
||||
if (!name) throw new Error(`Unbound value parameter ${type.parameterId}`);
|
||||
return name;
|
||||
}
|
||||
case "object-ref":
|
||||
return `QxObjectRef<${target(type.expectation, scope)}>`;
|
||||
case "record":
|
||||
return object(Object.entries(type.fields).map(([name, field]) => [name, value(field, scope, depth + 1)]));
|
||||
case "optional":
|
||||
return `(${value(type.value, scope, depth + 1)} | null)`;
|
||||
case "list":
|
||||
return `Array<${value(type.value, scope, depth + 1)}>`;
|
||||
case "alias": {
|
||||
const alias = scope.aliases.find((entry) => entry.id === type.definitionId);
|
||||
if (!alias) throw new Error(`Missing alias ${type.definitionId}`);
|
||||
return value(alias.body, bind(alias.parameters, type.arguments, scope), depth + 1);
|
||||
}
|
||||
default:
|
||||
return concrete(type);
|
||||
}
|
||||
};
|
||||
const params = (type: ValueTypeExpression, scope: Scope) =>
|
||||
type.kind === "builtin" && type.name === "unit" ? "" : `input:${value(type, scope)}`;
|
||||
const port = (entry: GenericDependencyPort): string => {
|
||||
const requirement = entry.requirement,
|
||||
scope = rootScope();
|
||||
switch (requirement.kind) {
|
||||
case "state":
|
||||
return object([
|
||||
...requirement.primitives.map((primitive): [string, string] => {
|
||||
if (primitive === "read") return ["get", `()=>Promise<${value(requirement.valueType, scope)}>`];
|
||||
if (primitive === "write") return ["set", `(value:${value(requirement.valueType, scope)})=>Promise<void>`];
|
||||
throw new Error(`Unsupported generic state primitive ${primitive}`);
|
||||
}),
|
||||
...(requirement.primitives.includes("read")
|
||||
? [["live", "()=>Promise<QxLiveValue>"] as [string, string]]
|
||||
: []),
|
||||
]);
|
||||
case "edge": {
|
||||
const ref = `QxObjectRef<${target(requirement.target, scope)}>`;
|
||||
const methods = requirement.primitives.map((primitive): [string, string] => {
|
||||
if (primitive === "resolve") return ["resolve", `()=>Promise<Array<${ref}>>`];
|
||||
if (primitive === "connect" || primitive === "disconnect")
|
||||
return [primitive, `(target:${ref})=>Promise<void>`];
|
||||
throw new Error(`Unsupported generic edge primitive ${primitive}`);
|
||||
});
|
||||
if (requirement.primitives.includes("resolve"))
|
||||
methods.push(["collection", `()=>Promise<RelationshipCollection<${ref}>>`]);
|
||||
if (
|
||||
["resolve", "connect", "disconnect"].every((primitive) =>
|
||||
requirement.primitives.includes(primitive as "resolve"),
|
||||
)
|
||||
)
|
||||
methods.push([
|
||||
"replace",
|
||||
`(entries:RelationshipEntry<${ref}>[],expectedRevision:bigint)=>Promise<RelationshipCollection<${ref}>>`,
|
||||
]);
|
||||
return object(methods);
|
||||
}
|
||||
case "constructor":
|
||||
return object([
|
||||
[
|
||||
"construct",
|
||||
`(${params(requirement.inputType, scope)})=>Promise<QxObjectRef<${target(requirement.target, scope)}>>`,
|
||||
],
|
||||
]);
|
||||
case "interface": {
|
||||
const contract = interfaces.find((entry) => entry.revisionId === requirement.application.definitionId);
|
||||
if (!contract) throw new Error(`Missing generic port contract ${requirement.application.definitionId}`);
|
||||
const local = {
|
||||
...bind(contract.template?.parameters ?? [], requirement.application.arguments, scope),
|
||||
aliases: contract.template?.aliases ?? [],
|
||||
};
|
||||
const operations = (contract.template?.members ?? contract.members).flatMap((member) =>
|
||||
member.operations
|
||||
.filter((op) => op.mode === "call")
|
||||
.map((op) => ({ ...op, name: `${member.displayName}.${op.displayName}` })),
|
||||
);
|
||||
return object([
|
||||
["objectId", `QxObjectRef<${target({ kind: "application", application: requirement.application }, scope)}>`],
|
||||
["live", object(operations.map((op) => [op.name, `(${params(op.inputType, local)})=>Promise<QxLiveValue>`]))],
|
||||
...operations.map(
|
||||
(op) =>
|
||||
[op.name, `(${params(op.inputType, local)})=>Promise<${value(op.outputType, local)}>`] as [
|
||||
string,
|
||||
string,
|
||||
],
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
const declarations = definition.parameters
|
||||
.map((parameter) => `${names.get(parameter.id)}${parameter.kind === "object" ? " extends string" : ""}`)
|
||||
.join(",");
|
||||
const receiver = definition.receiverRequirement;
|
||||
const context = object([
|
||||
["objectId", `QxObjectRef<${receiver.kind === "target" ? target(receiver.target, rootScope()) : "string"}>`],
|
||||
["input", value(definition.inputType, rootScope())],
|
||||
["ports", object(definition.dependencyPorts.map((entry) => [entry.displayName, port(entry)]))],
|
||||
]);
|
||||
const contextWithLifecycle = `${context} & QxContextLifecycle<${context} & {signal?: AbortSignal}>`;
|
||||
const result = value(definition.eventType ?? definition.outputType, rootScope());
|
||||
const handler = `<${declarations}>(context:${contextWithLifecycle})=>${result}|Promise<${result}>`;
|
||||
const derived = `{kind:"derived";get:${handler}}`;
|
||||
return definition.eventType
|
||||
? derived
|
||||
: `(${handler})${definition.kind === "operation" && definition.mode === "call" ? ` | ${derived}` : ""}`;
|
||||
};
|
||||
+78
-13
@@ -1,21 +1,74 @@
|
||||
import type { InterfaceRevision, PackageRevision, ValueType, DependencyPort } from "../capability-model/types.js";
|
||||
import type {
|
||||
InterfaceRevision,
|
||||
PackageRevision,
|
||||
ValueType,
|
||||
DependencyPort,
|
||||
WorkspaceRevision,
|
||||
} from "../capability-model/types.js";
|
||||
import type { CompiledCapabilityResourceRepository } from "../capability-language/assembly.js";
|
||||
import { genericImplementationType } from "./generics.js";
|
||||
|
||||
/** Portable generator input. New backends consume this instead of the parser or TS runtime. */
|
||||
export type BindingSchema = {
|
||||
format: "quixos-bindings";
|
||||
version: 1;
|
||||
interfaces: InterfaceRevision[];
|
||||
interfaceTemplates?: InterfaceRevision[];
|
||||
packages: PackageRevision[];
|
||||
};
|
||||
export const bindingSchema = (compiled: CompiledCapabilityResourceRepository): BindingSchema => ({
|
||||
export const bindingSchema = (compiled: Pick<CompiledCapabilityResourceRepository, "resources">): BindingSchema => ({
|
||||
format: "quixos-bindings",
|
||||
version: 1,
|
||||
interfaces: compiled.resources.flatMap((node) =>
|
||||
node.resource.kind === "interface" ? [node.resource.revision] : [],
|
||||
interfaceTemplates: compiled.resources.flatMap((node) =>
|
||||
node.resource.kind === "interface" && node.resource.revision.template ? [node.resource.revision] : [],
|
||||
),
|
||||
interfaces: [
|
||||
...new Map(
|
||||
compiled.resources
|
||||
.flatMap((node) => [
|
||||
...(node.resource.kind === "interface" ? [node.resource.revision] : []),
|
||||
...(node.resource.specializations ?? []),
|
||||
])
|
||||
.filter((entry) => !entry.template)
|
||||
.map((entry) => [entry.revisionId, entry]),
|
||||
).values(),
|
||||
],
|
||||
packages: compiled.resources.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])),
|
||||
});
|
||||
|
||||
export const specializeBindingSchema = (
|
||||
base: BindingSchema,
|
||||
workspace: WorkspaceRevision,
|
||||
revisionId: string,
|
||||
): BindingSchema => {
|
||||
const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId);
|
||||
if (!pkg) throw new Error(`Missing candidate package ${revisionId}`);
|
||||
const needed = new Set<string>();
|
||||
const visit = (value: unknown): void => {
|
||||
if (!value || typeof value !== "object") return;
|
||||
if ("interfaceRevisionId" in value && typeof value.interfaceRevisionId === "string")
|
||||
needed.add(value.interfaceRevisionId);
|
||||
Object.values(value).forEach(visit);
|
||||
};
|
||||
visit(pkg.exports);
|
||||
const interfaces = new Map<string, InterfaceRevision>(base.interfaces.map((entry) => [entry.revisionId, entry]));
|
||||
for (const id of needed) {
|
||||
const iface = workspace.interfaceImports.find((entry) => entry.revisionId === id);
|
||||
if (iface) {
|
||||
interfaces.set(id, iface);
|
||||
visit(iface.members);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
interfaces: [...interfaces.values()],
|
||||
packages: base.packages.map((entry) => {
|
||||
const candidate = workspace.packageImports.find((value) => value.revisionId === entry.revisionId);
|
||||
if (!candidate) throw new Error(`Missing dependency package ${entry.revisionId}`);
|
||||
return candidate;
|
||||
}),
|
||||
};
|
||||
};
|
||||
export type TypeScriptBindingOptions = {
|
||||
runtimeModule?: string;
|
||||
/** Each export must implement MessageBinding<T>, providing both TS type and wire codec. */
|
||||
@@ -46,6 +99,8 @@ export const generateTypeScriptBindings = (
|
||||
) => {
|
||||
if (schema.format !== "quixos-bindings" || schema.version !== 1)
|
||||
throw new Error("Unsupported binding schema version");
|
||||
if (schema.interfaces.some((entry) => entry.template))
|
||||
throw new Error("Package bindings require closed interface applications, not generic definitions");
|
||||
const pkg = schema.packages.find((entry) => entry.revisionId === packageRevisionId);
|
||||
if (!pkg) throw new Error(`Unknown package revision ${packageRevisionId}`);
|
||||
const messages = new Map<string, string>();
|
||||
@@ -174,6 +229,7 @@ export const generateTypeScriptBindings = (
|
||||
const specs: Record<string, unknown> = {};
|
||||
const contexts: [string, string][] = [];
|
||||
const handlers: [string, string][] = [];
|
||||
const results: [string, string][] = [];
|
||||
for (const entry of exports) {
|
||||
if (names.has(entry.displayName)) throw new Error(`Duplicate export name ${entry.displayName}`);
|
||||
names.add(entry.displayName);
|
||||
@@ -200,13 +256,15 @@ export const generateTypeScriptBindings = (
|
||||
const event = entry.kind === "operation" ? entry.eventType : undefined;
|
||||
const contextType = `Contexts[${q(entry.displayName)}]`;
|
||||
const outputType = type(event ?? entry.outputType);
|
||||
results.push([entry.displayName, outputType]);
|
||||
// Watch-start handlers produce events through the runtime's derived stream protocol.
|
||||
handlers.push([
|
||||
entry.displayName,
|
||||
event
|
||||
? `QxDerived<${contextType}, ${outputType}>`
|
||||
: `QxHandler<${contextType}, ${outputType}>${entry.kind === "operation" && entry.mode === "call" ? ` | QxDerived<${contextType}, ${outputType}>` : ""}`,
|
||||
]);
|
||||
if (!entry.application)
|
||||
handlers.push([
|
||||
entry.displayName,
|
||||
event
|
||||
? `QxDerived<${contextType}, ${outputType}>`
|
||||
: `QxHandler<${contextType}, ${outputType}>${entry.kind === "operation" && entry.mode === "call" ? ` | QxDerived<${contextType}, ${outputType}>` : ""}`,
|
||||
]);
|
||||
specs[entry.displayName] = {
|
||||
inputType: entry.inputType,
|
||||
outputType: entry.outputType,
|
||||
@@ -214,13 +272,19 @@ export const generateTypeScriptBindings = (
|
||||
ports: Object.fromEntries(ports.map((port) => [port.name, port.spec])),
|
||||
};
|
||||
}
|
||||
for (const definition of pkg.genericExports ?? []) {
|
||||
handlers.push([
|
||||
definition.displayName,
|
||||
genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type),
|
||||
]);
|
||||
}
|
||||
const imports = [...messages].map(([id, alias]) => {
|
||||
const binding = options.messages![id]!;
|
||||
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(binding.export))
|
||||
throw new Error(`Invalid message binding export ${binding.export}`);
|
||||
return `import { ${binding.export} as ${alias} } from ${q(binding.module)};`;
|
||||
});
|
||||
const signatures = `${object(contexts)} ${object(handlers)}`;
|
||||
const signatures = `${object(contexts)} ${object(handlers)} ${object(results)}`;
|
||||
const typeImports = [
|
||||
"BindingValue",
|
||||
"QxObjectRef",
|
||||
@@ -237,14 +301,15 @@ export const generateTypeScriptBindings = (
|
||||
`import { ${exports.length ? "bindQxHandler, " : ""}${[...typeImports, "QxHandlerSpec", "QxMessages"].map((name) => `type ${name}`).join(", ")} } from ${q(options.runtimeModule ?? "@quixos/camino-package-runtime")};\n` +
|
||||
imports.join("\n") +
|
||||
`\nexport const packageRevisionId = ${q(pkg.revisionId)};\n` +
|
||||
`export type Contexts = ${object(contexts)};\nexport type Implementation = ${object(handlers)};\n` +
|
||||
`declare const appliedType: unique symbol;\ntype QxApplied<Definition extends string, Arguments extends readonly unknown[]> = string & {readonly [appliedType]: (value: [Definition, Arguments]) => [Definition, Arguments]};\n` +
|
||||
`export type Contexts = ${object(contexts)};\nexport type Results = ${object(results)};\nexport type Implementation = ${object(handlers)};\n` +
|
||||
`const messages = { ${[...messages].map(([id, alias]) => `${q(id)}: ${alias}`).join(", ")} } satisfies QxMessages;\n` +
|
||||
`const specs = ${JSON.stringify(specs, null, 2)} satisfies Record<string, QxHandlerSpec>;\n` +
|
||||
`export const createRuntime = (implementation: Implementation) => ({\n packageRevisionId,\n exports: {\n` +
|
||||
exports
|
||||
.map(
|
||||
(entry) =>
|
||||
` ${q(entry.id)}: bindQxHandler(specs[${q(entry.displayName)}], implementation[${q(entry.displayName)}], messages),`,
|
||||
` ${q(entry.id)}: bindQxHandler(specs[${q(entry.displayName)}], implementation[${q(entry.application ? pkg.genericExports!.find((definition) => definition.id === entry.application!.exportId)!.displayName : entry.displayName)}], messages),`,
|
||||
)
|
||||
.join("\n") +
|
||||
`\n },\n});\n`
|
||||
|
||||
@@ -162,7 +162,10 @@ const environmentFor = (
|
||||
),
|
||||
),
|
||||
interfaceClosure: exactRevisions(
|
||||
closure.flatMap((node) => (node.resource.kind === "interface" ? [node.resource.revision] : [])),
|
||||
closure.flatMap((node) => [
|
||||
...(node.resource.kind === "interface" ? [node.resource.revision] : []),
|
||||
...(node.resource.specializations ?? []),
|
||||
]),
|
||||
),
|
||||
packageClosure: exactRevisions(
|
||||
closure.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])),
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type EvolutionReview,
|
||||
type WorkspaceRevision,
|
||||
} from "../capability-model/index.js";
|
||||
import { bindingSchema } from "../bindings/index.js";
|
||||
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
|
||||
import { snapshotCommit, checkoutCommit, buildCheckedPackage } from "./checked-build.js";
|
||||
const execFile = promisify(execFileCallback);
|
||||
const bytesDigest = (value: Uint8Array) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
||||
@@ -219,7 +219,12 @@ export const checkWorkspaceCandidate = async (options: {
|
||||
resolveResource,
|
||||
});
|
||||
const schema = path.join(temporary, "bindings.json");
|
||||
await fs.writeFile(schema, JSON.stringify(bindingSchema(candidate)));
|
||||
await fs.writeFile(
|
||||
schema,
|
||||
JSON.stringify(
|
||||
specializeBindingSchema(bindingSchema(candidate), compiled.workspace, candidate.resource.revision.revisionId),
|
||||
),
|
||||
);
|
||||
const artifactPath = await buildCheckedPackage(
|
||||
resource.directory,
|
||||
schema,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,213 +1,227 @@
|
||||
WORKSPACE=1
|
||||
FRAGMENT=2
|
||||
IMPORT=3
|
||||
EXTERNAL=4
|
||||
ATOM=5
|
||||
INTERFACE=6
|
||||
INTERFACES=7
|
||||
PACKAGE=8
|
||||
VALUE=9
|
||||
RELATION=10
|
||||
OPERATION=11
|
||||
FUNCTION=12
|
||||
CONSTRUCTOR=13
|
||||
CONSTRUCTS=14
|
||||
INPUT=15
|
||||
CONFORM=16
|
||||
AS=17
|
||||
BIND=18
|
||||
TO=19
|
||||
PRIVATE=20
|
||||
SHARED=21
|
||||
STATE=22
|
||||
EDGE=23
|
||||
PROJECTION=24
|
||||
WITH=25
|
||||
USING=26
|
||||
VIA=27
|
||||
MATERIALIZE=28
|
||||
IF=29
|
||||
ABSENT=30
|
||||
ON=31
|
||||
POLICY=32
|
||||
DEFAULT=33
|
||||
SOURCE=34
|
||||
REPOSITORY=35
|
||||
COMMIT=36
|
||||
REVISION=37
|
||||
SEMANTIC_MAJOR=38
|
||||
ON_DELETE=39
|
||||
RETAIN_OTHER=40
|
||||
KEYED=41
|
||||
PUBLIC_TRAVERSAL=42
|
||||
ID=43
|
||||
DOC=44
|
||||
MODE=45
|
||||
EMITS=46
|
||||
RECEIVER=47
|
||||
REQUIRES=48
|
||||
ANY=49
|
||||
GET=50
|
||||
SET=51
|
||||
WATCH=52
|
||||
START=53
|
||||
STOP=54
|
||||
READ=55
|
||||
WRITE=56
|
||||
RESOLVE=57
|
||||
CONNECT=58
|
||||
DISCONNECT=59
|
||||
CALL=60
|
||||
WATCH_START=61
|
||||
WATCH_STOP=62
|
||||
SUBSCRIBE=63
|
||||
UNSUBSCRIBE=64
|
||||
OPTIMISTIC_REGISTER=65
|
||||
CRDT=66
|
||||
OPTIONAL_ONE=67
|
||||
EXACTLY_ONE=68
|
||||
MANY_UNIQUE=69
|
||||
MANY=70
|
||||
ORDERED=71
|
||||
UNIT=72
|
||||
WATCH_HANDLE=73
|
||||
MESSAGE=74
|
||||
ATOM_REF=75
|
||||
INTERFACE_REF=76
|
||||
OPTIONAL=77
|
||||
LIST=78
|
||||
RECORD=79
|
||||
BOOL=80
|
||||
BYTES=81
|
||||
DOUBLE=82
|
||||
INT32=83
|
||||
INT64=84
|
||||
STRING=85
|
||||
UINT32=86
|
||||
UINT64=87
|
||||
TRUE=88
|
||||
FALSE=89
|
||||
NULL=90
|
||||
ARROW=91
|
||||
COLON=92
|
||||
SEMI=93
|
||||
COMMA=94
|
||||
DOT=95
|
||||
LBRACE=96
|
||||
RBRACE=97
|
||||
LBRACK=98
|
||||
RBRACK=99
|
||||
LPAREN=100
|
||||
RPAREN=101
|
||||
LT=102
|
||||
GT=103
|
||||
INTEGER=104
|
||||
JSON_NUMBER=105
|
||||
IDENTIFIER=106
|
||||
STRING_LITERAL=107
|
||||
LINE_COMMENT=108
|
||||
BLOCK_COMMENT=109
|
||||
WS=110
|
||||
TYPE=2
|
||||
OBJECT=3
|
||||
STORABLE=4
|
||||
IMPLEMENTS=5
|
||||
REF=6
|
||||
FRAGMENT=7
|
||||
IMPORT=8
|
||||
EXTERNAL=9
|
||||
ATOM=10
|
||||
INTERFACE=11
|
||||
INTERFACES=12
|
||||
PACKAGE=13
|
||||
VALUE=14
|
||||
RELATION=15
|
||||
OPERATION=16
|
||||
FUNCTION=17
|
||||
CONSTRUCTOR=18
|
||||
CONSTRUCTS=19
|
||||
INPUT=20
|
||||
CONFORM=21
|
||||
AS=22
|
||||
BIND=23
|
||||
TO=24
|
||||
PRIVATE=25
|
||||
SHARED=26
|
||||
STATE=27
|
||||
EDGE=28
|
||||
PROJECTION=29
|
||||
WITH=30
|
||||
USING=31
|
||||
VIA=32
|
||||
MATERIALIZE=33
|
||||
IF=34
|
||||
ABSENT=35
|
||||
ON=36
|
||||
POLICY=37
|
||||
DEFAULT=38
|
||||
SOURCE=39
|
||||
REPOSITORY=40
|
||||
COMMIT=41
|
||||
REVISION=42
|
||||
SEMANTIC_MAJOR=43
|
||||
ON_DELETE=44
|
||||
RETAIN_OTHER=45
|
||||
KEYED=46
|
||||
PUBLIC_TRAVERSAL=47
|
||||
ID=48
|
||||
DOC=49
|
||||
MODE=50
|
||||
EMITS=51
|
||||
RECEIVER=52
|
||||
REQUIRES=53
|
||||
ANY=54
|
||||
GET=55
|
||||
SET=56
|
||||
WATCH=57
|
||||
START=58
|
||||
STOP=59
|
||||
READ=60
|
||||
WRITE=61
|
||||
RESOLVE=62
|
||||
CONNECT=63
|
||||
DISCONNECT=64
|
||||
CALL=65
|
||||
WATCH_START=66
|
||||
WATCH_STOP=67
|
||||
SUBSCRIBE=68
|
||||
UNSUBSCRIBE=69
|
||||
OPTIMISTIC_REGISTER=70
|
||||
CRDT=71
|
||||
OPTIONAL_ONE=72
|
||||
EXACTLY_ONE=73
|
||||
MANY_UNIQUE=74
|
||||
MANY=75
|
||||
ORDERED=76
|
||||
UNIT=77
|
||||
WATCH_HANDLE=78
|
||||
MESSAGE=79
|
||||
ATOM_REF=80
|
||||
INTERFACE_REF=81
|
||||
OPTIONAL=82
|
||||
LIST=83
|
||||
RECORD=84
|
||||
BOOL=85
|
||||
BYTES=86
|
||||
DOUBLE=87
|
||||
INT32=88
|
||||
INT64=89
|
||||
STRING=90
|
||||
UINT32=91
|
||||
UINT64=92
|
||||
TRUE=93
|
||||
FALSE=94
|
||||
NULL=95
|
||||
ARROW=96
|
||||
COLON=97
|
||||
SEMI=98
|
||||
COMMA=99
|
||||
DOT=100
|
||||
LBRACE=101
|
||||
RBRACE=102
|
||||
LBRACK=103
|
||||
RBRACK=104
|
||||
LPAREN=105
|
||||
RPAREN=106
|
||||
LT=107
|
||||
GT=108
|
||||
AMP=109
|
||||
EQUAL=110
|
||||
INTEGER=111
|
||||
JSON_NUMBER=112
|
||||
IDENTIFIER=113
|
||||
STRING_LITERAL=114
|
||||
LINE_COMMENT=115
|
||||
BLOCK_COMMENT=116
|
||||
WS=117
|
||||
'workspace'=1
|
||||
'fragment'=2
|
||||
'import'=3
|
||||
'external'=4
|
||||
'atom'=5
|
||||
'interface'=6
|
||||
'interfaces'=7
|
||||
'package'=8
|
||||
'value'=9
|
||||
'relation'=10
|
||||
'operation'=11
|
||||
'function'=12
|
||||
'constructor'=13
|
||||
'constructs'=14
|
||||
'input'=15
|
||||
'conform'=16
|
||||
'as'=17
|
||||
'bind'=18
|
||||
'to'=19
|
||||
'private'=20
|
||||
'shared'=21
|
||||
'state'=22
|
||||
'edge'=23
|
||||
'projection'=24
|
||||
'with'=25
|
||||
'using'=26
|
||||
'via'=27
|
||||
'materialize'=28
|
||||
'if'=29
|
||||
'absent'=30
|
||||
'on'=31
|
||||
'policy'=32
|
||||
'default'=33
|
||||
'source'=34
|
||||
'repository'=35
|
||||
'commit'=36
|
||||
'revision'=37
|
||||
'semantic-major'=38
|
||||
'on-delete'=39
|
||||
'retain-other'=40
|
||||
'keyed'=41
|
||||
'public-traversal'=42
|
||||
'id'=43
|
||||
'doc'=44
|
||||
'mode'=45
|
||||
'emits'=46
|
||||
'receiver'=47
|
||||
'requires'=48
|
||||
'any'=49
|
||||
'get'=50
|
||||
'set'=51
|
||||
'watch'=52
|
||||
'start'=53
|
||||
'stop'=54
|
||||
'read'=55
|
||||
'write'=56
|
||||
'resolve'=57
|
||||
'connect'=58
|
||||
'disconnect'=59
|
||||
'call'=60
|
||||
'watch-start'=61
|
||||
'watch-stop'=62
|
||||
'subscribe'=63
|
||||
'unsubscribe'=64
|
||||
'optimistic-register'=65
|
||||
'crdt'=66
|
||||
'optional-one'=67
|
||||
'exactly-one'=68
|
||||
'many-unique'=69
|
||||
'many'=70
|
||||
'ordered'=71
|
||||
'unit'=72
|
||||
'watch-handle'=73
|
||||
'message'=74
|
||||
'atom-ref'=75
|
||||
'interface-ref'=76
|
||||
'optional'=77
|
||||
'list'=78
|
||||
'record'=79
|
||||
'bool'=80
|
||||
'bytes'=81
|
||||
'double'=82
|
||||
'int32'=83
|
||||
'int64'=84
|
||||
'string'=85
|
||||
'uint32'=86
|
||||
'uint64'=87
|
||||
'true'=88
|
||||
'false'=89
|
||||
'null'=90
|
||||
'->'=91
|
||||
':'=92
|
||||
';'=93
|
||||
','=94
|
||||
'.'=95
|
||||
'{'=96
|
||||
'}'=97
|
||||
'['=98
|
||||
']'=99
|
||||
'('=100
|
||||
')'=101
|
||||
'<'=102
|
||||
'>'=103
|
||||
'type'=2
|
||||
'object'=3
|
||||
'storable'=4
|
||||
'implements'=5
|
||||
'ref'=6
|
||||
'fragment'=7
|
||||
'import'=8
|
||||
'external'=9
|
||||
'atom'=10
|
||||
'interface'=11
|
||||
'interfaces'=12
|
||||
'package'=13
|
||||
'value'=14
|
||||
'relation'=15
|
||||
'operation'=16
|
||||
'function'=17
|
||||
'constructor'=18
|
||||
'constructs'=19
|
||||
'input'=20
|
||||
'conform'=21
|
||||
'as'=22
|
||||
'bind'=23
|
||||
'to'=24
|
||||
'private'=25
|
||||
'shared'=26
|
||||
'state'=27
|
||||
'edge'=28
|
||||
'projection'=29
|
||||
'with'=30
|
||||
'using'=31
|
||||
'via'=32
|
||||
'materialize'=33
|
||||
'if'=34
|
||||
'absent'=35
|
||||
'on'=36
|
||||
'policy'=37
|
||||
'default'=38
|
||||
'source'=39
|
||||
'repository'=40
|
||||
'commit'=41
|
||||
'revision'=42
|
||||
'semantic-major'=43
|
||||
'on-delete'=44
|
||||
'retain-other'=45
|
||||
'keyed'=46
|
||||
'public-traversal'=47
|
||||
'id'=48
|
||||
'doc'=49
|
||||
'mode'=50
|
||||
'emits'=51
|
||||
'receiver'=52
|
||||
'requires'=53
|
||||
'any'=54
|
||||
'get'=55
|
||||
'set'=56
|
||||
'watch'=57
|
||||
'start'=58
|
||||
'stop'=59
|
||||
'read'=60
|
||||
'write'=61
|
||||
'resolve'=62
|
||||
'connect'=63
|
||||
'disconnect'=64
|
||||
'call'=65
|
||||
'watch-start'=66
|
||||
'watch-stop'=67
|
||||
'subscribe'=68
|
||||
'unsubscribe'=69
|
||||
'optimistic-register'=70
|
||||
'crdt'=71
|
||||
'optional-one'=72
|
||||
'exactly-one'=73
|
||||
'many-unique'=74
|
||||
'many'=75
|
||||
'ordered'=76
|
||||
'unit'=77
|
||||
'watch-handle'=78
|
||||
'message'=79
|
||||
'atom-ref'=80
|
||||
'interface-ref'=81
|
||||
'optional'=82
|
||||
'list'=83
|
||||
'record'=84
|
||||
'bool'=85
|
||||
'bytes'=86
|
||||
'double'=87
|
||||
'int32'=88
|
||||
'int64'=89
|
||||
'string'=90
|
||||
'uint32'=91
|
||||
'uint64'=92
|
||||
'true'=93
|
||||
'false'=94
|
||||
'null'=95
|
||||
'->'=96
|
||||
':'=97
|
||||
';'=98
|
||||
','=99
|
||||
'.'=100
|
||||
'{'=101
|
||||
'}'=102
|
||||
'['=103
|
||||
']'=104
|
||||
'('=105
|
||||
')'=106
|
||||
'<'=107
|
||||
'>'=108
|
||||
'&'=109
|
||||
'='=110
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,213 +1,227 @@
|
||||
WORKSPACE=1
|
||||
FRAGMENT=2
|
||||
IMPORT=3
|
||||
EXTERNAL=4
|
||||
ATOM=5
|
||||
INTERFACE=6
|
||||
INTERFACES=7
|
||||
PACKAGE=8
|
||||
VALUE=9
|
||||
RELATION=10
|
||||
OPERATION=11
|
||||
FUNCTION=12
|
||||
CONSTRUCTOR=13
|
||||
CONSTRUCTS=14
|
||||
INPUT=15
|
||||
CONFORM=16
|
||||
AS=17
|
||||
BIND=18
|
||||
TO=19
|
||||
PRIVATE=20
|
||||
SHARED=21
|
||||
STATE=22
|
||||
EDGE=23
|
||||
PROJECTION=24
|
||||
WITH=25
|
||||
USING=26
|
||||
VIA=27
|
||||
MATERIALIZE=28
|
||||
IF=29
|
||||
ABSENT=30
|
||||
ON=31
|
||||
POLICY=32
|
||||
DEFAULT=33
|
||||
SOURCE=34
|
||||
REPOSITORY=35
|
||||
COMMIT=36
|
||||
REVISION=37
|
||||
SEMANTIC_MAJOR=38
|
||||
ON_DELETE=39
|
||||
RETAIN_OTHER=40
|
||||
KEYED=41
|
||||
PUBLIC_TRAVERSAL=42
|
||||
ID=43
|
||||
DOC=44
|
||||
MODE=45
|
||||
EMITS=46
|
||||
RECEIVER=47
|
||||
REQUIRES=48
|
||||
ANY=49
|
||||
GET=50
|
||||
SET=51
|
||||
WATCH=52
|
||||
START=53
|
||||
STOP=54
|
||||
READ=55
|
||||
WRITE=56
|
||||
RESOLVE=57
|
||||
CONNECT=58
|
||||
DISCONNECT=59
|
||||
CALL=60
|
||||
WATCH_START=61
|
||||
WATCH_STOP=62
|
||||
SUBSCRIBE=63
|
||||
UNSUBSCRIBE=64
|
||||
OPTIMISTIC_REGISTER=65
|
||||
CRDT=66
|
||||
OPTIONAL_ONE=67
|
||||
EXACTLY_ONE=68
|
||||
MANY_UNIQUE=69
|
||||
MANY=70
|
||||
ORDERED=71
|
||||
UNIT=72
|
||||
WATCH_HANDLE=73
|
||||
MESSAGE=74
|
||||
ATOM_REF=75
|
||||
INTERFACE_REF=76
|
||||
OPTIONAL=77
|
||||
LIST=78
|
||||
RECORD=79
|
||||
BOOL=80
|
||||
BYTES=81
|
||||
DOUBLE=82
|
||||
INT32=83
|
||||
INT64=84
|
||||
STRING=85
|
||||
UINT32=86
|
||||
UINT64=87
|
||||
TRUE=88
|
||||
FALSE=89
|
||||
NULL=90
|
||||
ARROW=91
|
||||
COLON=92
|
||||
SEMI=93
|
||||
COMMA=94
|
||||
DOT=95
|
||||
LBRACE=96
|
||||
RBRACE=97
|
||||
LBRACK=98
|
||||
RBRACK=99
|
||||
LPAREN=100
|
||||
RPAREN=101
|
||||
LT=102
|
||||
GT=103
|
||||
INTEGER=104
|
||||
JSON_NUMBER=105
|
||||
IDENTIFIER=106
|
||||
STRING_LITERAL=107
|
||||
LINE_COMMENT=108
|
||||
BLOCK_COMMENT=109
|
||||
WS=110
|
||||
TYPE=2
|
||||
OBJECT=3
|
||||
STORABLE=4
|
||||
IMPLEMENTS=5
|
||||
REF=6
|
||||
FRAGMENT=7
|
||||
IMPORT=8
|
||||
EXTERNAL=9
|
||||
ATOM=10
|
||||
INTERFACE=11
|
||||
INTERFACES=12
|
||||
PACKAGE=13
|
||||
VALUE=14
|
||||
RELATION=15
|
||||
OPERATION=16
|
||||
FUNCTION=17
|
||||
CONSTRUCTOR=18
|
||||
CONSTRUCTS=19
|
||||
INPUT=20
|
||||
CONFORM=21
|
||||
AS=22
|
||||
BIND=23
|
||||
TO=24
|
||||
PRIVATE=25
|
||||
SHARED=26
|
||||
STATE=27
|
||||
EDGE=28
|
||||
PROJECTION=29
|
||||
WITH=30
|
||||
USING=31
|
||||
VIA=32
|
||||
MATERIALIZE=33
|
||||
IF=34
|
||||
ABSENT=35
|
||||
ON=36
|
||||
POLICY=37
|
||||
DEFAULT=38
|
||||
SOURCE=39
|
||||
REPOSITORY=40
|
||||
COMMIT=41
|
||||
REVISION=42
|
||||
SEMANTIC_MAJOR=43
|
||||
ON_DELETE=44
|
||||
RETAIN_OTHER=45
|
||||
KEYED=46
|
||||
PUBLIC_TRAVERSAL=47
|
||||
ID=48
|
||||
DOC=49
|
||||
MODE=50
|
||||
EMITS=51
|
||||
RECEIVER=52
|
||||
REQUIRES=53
|
||||
ANY=54
|
||||
GET=55
|
||||
SET=56
|
||||
WATCH=57
|
||||
START=58
|
||||
STOP=59
|
||||
READ=60
|
||||
WRITE=61
|
||||
RESOLVE=62
|
||||
CONNECT=63
|
||||
DISCONNECT=64
|
||||
CALL=65
|
||||
WATCH_START=66
|
||||
WATCH_STOP=67
|
||||
SUBSCRIBE=68
|
||||
UNSUBSCRIBE=69
|
||||
OPTIMISTIC_REGISTER=70
|
||||
CRDT=71
|
||||
OPTIONAL_ONE=72
|
||||
EXACTLY_ONE=73
|
||||
MANY_UNIQUE=74
|
||||
MANY=75
|
||||
ORDERED=76
|
||||
UNIT=77
|
||||
WATCH_HANDLE=78
|
||||
MESSAGE=79
|
||||
ATOM_REF=80
|
||||
INTERFACE_REF=81
|
||||
OPTIONAL=82
|
||||
LIST=83
|
||||
RECORD=84
|
||||
BOOL=85
|
||||
BYTES=86
|
||||
DOUBLE=87
|
||||
INT32=88
|
||||
INT64=89
|
||||
STRING=90
|
||||
UINT32=91
|
||||
UINT64=92
|
||||
TRUE=93
|
||||
FALSE=94
|
||||
NULL=95
|
||||
ARROW=96
|
||||
COLON=97
|
||||
SEMI=98
|
||||
COMMA=99
|
||||
DOT=100
|
||||
LBRACE=101
|
||||
RBRACE=102
|
||||
LBRACK=103
|
||||
RBRACK=104
|
||||
LPAREN=105
|
||||
RPAREN=106
|
||||
LT=107
|
||||
GT=108
|
||||
AMP=109
|
||||
EQUAL=110
|
||||
INTEGER=111
|
||||
JSON_NUMBER=112
|
||||
IDENTIFIER=113
|
||||
STRING_LITERAL=114
|
||||
LINE_COMMENT=115
|
||||
BLOCK_COMMENT=116
|
||||
WS=117
|
||||
'workspace'=1
|
||||
'fragment'=2
|
||||
'import'=3
|
||||
'external'=4
|
||||
'atom'=5
|
||||
'interface'=6
|
||||
'interfaces'=7
|
||||
'package'=8
|
||||
'value'=9
|
||||
'relation'=10
|
||||
'operation'=11
|
||||
'function'=12
|
||||
'constructor'=13
|
||||
'constructs'=14
|
||||
'input'=15
|
||||
'conform'=16
|
||||
'as'=17
|
||||
'bind'=18
|
||||
'to'=19
|
||||
'private'=20
|
||||
'shared'=21
|
||||
'state'=22
|
||||
'edge'=23
|
||||
'projection'=24
|
||||
'with'=25
|
||||
'using'=26
|
||||
'via'=27
|
||||
'materialize'=28
|
||||
'if'=29
|
||||
'absent'=30
|
||||
'on'=31
|
||||
'policy'=32
|
||||
'default'=33
|
||||
'source'=34
|
||||
'repository'=35
|
||||
'commit'=36
|
||||
'revision'=37
|
||||
'semantic-major'=38
|
||||
'on-delete'=39
|
||||
'retain-other'=40
|
||||
'keyed'=41
|
||||
'public-traversal'=42
|
||||
'id'=43
|
||||
'doc'=44
|
||||
'mode'=45
|
||||
'emits'=46
|
||||
'receiver'=47
|
||||
'requires'=48
|
||||
'any'=49
|
||||
'get'=50
|
||||
'set'=51
|
||||
'watch'=52
|
||||
'start'=53
|
||||
'stop'=54
|
||||
'read'=55
|
||||
'write'=56
|
||||
'resolve'=57
|
||||
'connect'=58
|
||||
'disconnect'=59
|
||||
'call'=60
|
||||
'watch-start'=61
|
||||
'watch-stop'=62
|
||||
'subscribe'=63
|
||||
'unsubscribe'=64
|
||||
'optimistic-register'=65
|
||||
'crdt'=66
|
||||
'optional-one'=67
|
||||
'exactly-one'=68
|
||||
'many-unique'=69
|
||||
'many'=70
|
||||
'ordered'=71
|
||||
'unit'=72
|
||||
'watch-handle'=73
|
||||
'message'=74
|
||||
'atom-ref'=75
|
||||
'interface-ref'=76
|
||||
'optional'=77
|
||||
'list'=78
|
||||
'record'=79
|
||||
'bool'=80
|
||||
'bytes'=81
|
||||
'double'=82
|
||||
'int32'=83
|
||||
'int64'=84
|
||||
'string'=85
|
||||
'uint32'=86
|
||||
'uint64'=87
|
||||
'true'=88
|
||||
'false'=89
|
||||
'null'=90
|
||||
'->'=91
|
||||
':'=92
|
||||
';'=93
|
||||
','=94
|
||||
'.'=95
|
||||
'{'=96
|
||||
'}'=97
|
||||
'['=98
|
||||
']'=99
|
||||
'('=100
|
||||
')'=101
|
||||
'<'=102
|
||||
'>'=103
|
||||
'type'=2
|
||||
'object'=3
|
||||
'storable'=4
|
||||
'implements'=5
|
||||
'ref'=6
|
||||
'fragment'=7
|
||||
'import'=8
|
||||
'external'=9
|
||||
'atom'=10
|
||||
'interface'=11
|
||||
'interfaces'=12
|
||||
'package'=13
|
||||
'value'=14
|
||||
'relation'=15
|
||||
'operation'=16
|
||||
'function'=17
|
||||
'constructor'=18
|
||||
'constructs'=19
|
||||
'input'=20
|
||||
'conform'=21
|
||||
'as'=22
|
||||
'bind'=23
|
||||
'to'=24
|
||||
'private'=25
|
||||
'shared'=26
|
||||
'state'=27
|
||||
'edge'=28
|
||||
'projection'=29
|
||||
'with'=30
|
||||
'using'=31
|
||||
'via'=32
|
||||
'materialize'=33
|
||||
'if'=34
|
||||
'absent'=35
|
||||
'on'=36
|
||||
'policy'=37
|
||||
'default'=38
|
||||
'source'=39
|
||||
'repository'=40
|
||||
'commit'=41
|
||||
'revision'=42
|
||||
'semantic-major'=43
|
||||
'on-delete'=44
|
||||
'retain-other'=45
|
||||
'keyed'=46
|
||||
'public-traversal'=47
|
||||
'id'=48
|
||||
'doc'=49
|
||||
'mode'=50
|
||||
'emits'=51
|
||||
'receiver'=52
|
||||
'requires'=53
|
||||
'any'=54
|
||||
'get'=55
|
||||
'set'=56
|
||||
'watch'=57
|
||||
'start'=58
|
||||
'stop'=59
|
||||
'read'=60
|
||||
'write'=61
|
||||
'resolve'=62
|
||||
'connect'=63
|
||||
'disconnect'=64
|
||||
'call'=65
|
||||
'watch-start'=66
|
||||
'watch-stop'=67
|
||||
'subscribe'=68
|
||||
'unsubscribe'=69
|
||||
'optimistic-register'=70
|
||||
'crdt'=71
|
||||
'optional-one'=72
|
||||
'exactly-one'=73
|
||||
'many-unique'=74
|
||||
'many'=75
|
||||
'ordered'=76
|
||||
'unit'=77
|
||||
'watch-handle'=78
|
||||
'message'=79
|
||||
'atom-ref'=80
|
||||
'interface-ref'=81
|
||||
'optional'=82
|
||||
'list'=83
|
||||
'record'=84
|
||||
'bool'=85
|
||||
'bytes'=86
|
||||
'double'=87
|
||||
'int32'=88
|
||||
'int64'=89
|
||||
'string'=90
|
||||
'uint32'=91
|
||||
'uint64'=92
|
||||
'true'=93
|
||||
'false'=94
|
||||
'null'=95
|
||||
'->'=96
|
||||
':'=97
|
||||
';'=98
|
||||
','=99
|
||||
'.'=100
|
||||
'{'=101
|
||||
'}'=102
|
||||
'['=103
|
||||
']'=104
|
||||
'('=105
|
||||
')'=106
|
||||
'<'=107
|
||||
'>'=108
|
||||
'&'=109
|
||||
'='=110
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,12 @@ import { ExternalInterfaceDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { ResourcePreambleContext } from "./QuixosCapabilityParser.js";
|
||||
import { AtomDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { InterfaceResourceDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { TypeParametersContext } from "./QuixosCapabilityParser.js";
|
||||
import { TypeParameterContext } from "./QuixosCapabilityParser.js";
|
||||
import { InterfaceTypeContext } from "./QuixosCapabilityParser.js";
|
||||
import { TypeArgumentsContext } from "./QuixosCapabilityParser.js";
|
||||
import { TypeArgumentContext } from "./QuixosCapabilityParser.js";
|
||||
import { TypeAliasDeclContext } from "./QuixosCapabilityParser.js";
|
||||
import { InterfaceMemberContext } from "./QuixosCapabilityParser.js";
|
||||
import { OperationMemberContext } from "./QuixosCapabilityParser.js";
|
||||
import { ValueMemberContext } from "./QuixosCapabilityParser.js";
|
||||
@@ -137,6 +143,42 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitInterfaceResourceDecl?: (ctx: InterfaceResourceDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.typeParameters`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTypeParameters?: (ctx: TypeParametersContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.typeParameter`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTypeParameter?: (ctx: TypeParameterContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceType`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitInterfaceType?: (ctx: InterfaceTypeContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.typeArguments`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTypeArguments?: (ctx: TypeArgumentsContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.typeArgument`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTypeArgument?: (ctx: TypeArgumentContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.typeAliasDecl`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitTypeAliasDecl?: (ctx: TypeAliasDeclContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `QuixosCapabilityParser.interfaceMember`.
|
||||
* @param ctx the parse tree
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
import {
|
||||
TypeSubstitution,
|
||||
GenericTypeError,
|
||||
instantiateInterface,
|
||||
appliedInterfaceId,
|
||||
valueType,
|
||||
type AtomId,
|
||||
type ClosedTypeArgument,
|
||||
type GenericTypeEnvironment,
|
||||
type InterfaceApplicationExpression,
|
||||
type InterfaceRevision,
|
||||
type ObjectTypeExpression,
|
||||
type TypeArgumentExpression,
|
||||
type TypeParameter,
|
||||
type ValueAliasDefinition,
|
||||
type ValueType,
|
||||
type ValueTypeExpression,
|
||||
} from "../capability-model/index.js";
|
||||
import type {
|
||||
InterfaceTypeContext,
|
||||
TargetConstraintContext,
|
||||
TypeArgumentsContext,
|
||||
TypeParametersContext,
|
||||
TypeAliasDeclContext,
|
||||
ValueTypeContext,
|
||||
} from "./generated/QuixosCapabilityParser.js";
|
||||
|
||||
const literal = (context: { getText(): string }) => JSON.parse(context.getText()) as string;
|
||||
|
||||
/** Lexical authoring scope; only `value`/`target` return installed types. */
|
||||
export class GenericSourceTypes {
|
||||
readonly interfaces = new Map<string, InterfaceRevision>();
|
||||
readonly definitions = new Map<string, InterfaceRevision>();
|
||||
readonly applications = new Map<string, InterfaceRevision>();
|
||||
readonly aliases = new Map<string, ValueAliasDefinition>();
|
||||
parameters = new Map<string, TypeParameter>();
|
||||
self?: AtomId;
|
||||
private active: string[] = [];
|
||||
private applicationCount = 0;
|
||||
|
||||
constructor(readonly atoms: Map<string, AtomId>) {}
|
||||
|
||||
environment(): GenericTypeEnvironment {
|
||||
return {
|
||||
arguments: new Map(),
|
||||
aliases: this.aliases,
|
||||
self: this.self,
|
||||
applyInterface: (id, args) => this.apply(id, args).revisionId,
|
||||
// Obligations are retained on the application and discharged by workspace
|
||||
// validation, where all atom conformances are known (not source order).
|
||||
implementsInterface: () => true,
|
||||
};
|
||||
}
|
||||
|
||||
register(name: string, definition: InterfaceRevision) {
|
||||
this.interfaces.set(name, definition);
|
||||
this.definitions.set(definition.revisionId, definition);
|
||||
}
|
||||
|
||||
/** Check authored applications even when nobody has instantiated this template yet. */
|
||||
validateTemplate(definition: InterfaceRevision) {
|
||||
const template = definition.template;
|
||||
if (!template) return;
|
||||
const aliases = new Map((template.aliases ?? []).map((alias) => [alias.id, alias]));
|
||||
const parametersById = new Map(
|
||||
[...template.parameters, ...(template.aliases ?? []).flatMap((alias) => alias.parameters)].map((parameter) => [
|
||||
parameter.id,
|
||||
parameter,
|
||||
]),
|
||||
);
|
||||
const replace = (node: unknown, arguments_: Map<string, TypeArgumentExpression>): unknown => {
|
||||
if (!node || typeof node !== "object") return node;
|
||||
if ("kind" in node && node.kind === "parameter" && "parameterId" in node) {
|
||||
const arg = arguments_.get(String(node.parameterId));
|
||||
if (arg) return arg.kind === "value" ? arg.type : arg.target;
|
||||
}
|
||||
if (Array.isArray(node)) return node.map((entry) => replace(entry, arguments_));
|
||||
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, replace(value, arguments_)]));
|
||||
};
|
||||
const implies = (
|
||||
actual: InterfaceApplicationExpression,
|
||||
required: InterfaceApplicationExpression,
|
||||
seen = new Set<string>(),
|
||||
): boolean => {
|
||||
const key = JSON.stringify(actual);
|
||||
if (key === JSON.stringify(required)) return true;
|
||||
if (seen.has(key) || seen.size > 128) return false;
|
||||
seen.add(key);
|
||||
const contract = this.definitions.get(actual.definitionId);
|
||||
if (!contract?.template)
|
||||
return (contract?.requiredInterfaces ?? []).includes(required.definitionId) && required.arguments.length === 0;
|
||||
const args = new Map(
|
||||
contract.template.parameters.map((parameter, index) => [parameter.id, actual.arguments[index]]),
|
||||
);
|
||||
return contract.template.requires.some((parent) =>
|
||||
implies(replace(parent, args) as InterfaceApplicationExpression, required, seen),
|
||||
);
|
||||
};
|
||||
const storable = (type: ValueTypeExpression, depth = 0): boolean => {
|
||||
if (depth > 128)
|
||||
throw new GenericTypeError(
|
||||
"type-complexity-limit",
|
||||
definition.displayName,
|
||||
"Storable alias expansion exceeds depth limit",
|
||||
);
|
||||
if (type.kind === "parameter") {
|
||||
const parameter = parametersById.get(type.parameterId);
|
||||
return parameter?.kind === "value" && Boolean(parameter.storable);
|
||||
}
|
||||
if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1);
|
||||
if (type.kind === "alias") {
|
||||
const alias = aliases.get(type.definitionId);
|
||||
if (!alias || alias.parameters.length !== type.arguments.length) return false;
|
||||
return storable(
|
||||
replace(
|
||||
alias.body,
|
||||
new Map(alias.parameters.map((parameter, index) => [parameter.id, type.arguments[index]])),
|
||||
) as ValueTypeExpression,
|
||||
depth + 1,
|
||||
);
|
||||
}
|
||||
return type.kind === "scalar" || (type.kind === "builtin" && type.name === "unit");
|
||||
};
|
||||
let remaining = 10000;
|
||||
const visit = (node: unknown, path: string, depth = 0): void => {
|
||||
if (depth > 128 || --remaining < 0)
|
||||
throw new GenericTypeError("type-complexity-limit", path, "Type exceeds the depth or expansion budget");
|
||||
if (!node || typeof node !== "object") return;
|
||||
if ("definitionId" in node && "arguments" in node) {
|
||||
const application = node as InterfaceApplicationExpression | Extract<ValueTypeExpression, { kind: "alias" }>;
|
||||
const alias = "kind" in application && application.kind === "alias";
|
||||
const target = alias ? aliases.get(application.definitionId) : this.definitions.get(application.definitionId);
|
||||
if (!target)
|
||||
throw new GenericTypeError(
|
||||
alias ? "unknown-alias" : "unknown-interface",
|
||||
path,
|
||||
`Unknown definition ${application.definitionId}`,
|
||||
);
|
||||
if ("template" in target && target.template?.usesSelf) template.usesSelf = true;
|
||||
const parameters = "parameters" in target ? target.parameters : (target.template?.parameters ?? []);
|
||||
if (parameters.length !== application.arguments.length)
|
||||
throw new GenericTypeError(
|
||||
"type-arity",
|
||||
path,
|
||||
`Expected ${parameters.length} type arguments, received ${application.arguments.length}`,
|
||||
);
|
||||
parameters.forEach((parameter, index) => {
|
||||
if (parameter.kind !== application.arguments[index].kind)
|
||||
throw new GenericTypeError(
|
||||
"parameter-kind",
|
||||
path,
|
||||
`Expected ${parameter.kind}, received ${application.arguments[index].kind}`,
|
||||
);
|
||||
const argument = application.arguments[index];
|
||||
if (parameter.kind === "value" && parameter.storable && argument.kind === "value" && !storable(argument.type))
|
||||
throw new GenericTypeError(
|
||||
"non-storable-argument",
|
||||
path,
|
||||
"Generic application cannot prove its value argument is storable",
|
||||
);
|
||||
if (parameter.kind === "object" && argument.kind === "object" && argument.target.kind === "parameter") {
|
||||
const offered = parametersById.get(argument.target.parameterId);
|
||||
const mapping = new Map(parameters.map((p, i) => [p.id, application.arguments[i]]));
|
||||
for (const bound of parameter.implements) {
|
||||
const required = replace(bound, mapping) as InterfaceApplicationExpression;
|
||||
if (offered?.kind !== "object" || !offered.implements.some((evidence) => implies(evidence, required)))
|
||||
throw new GenericTypeError(
|
||||
"unsatisfied-bound",
|
||||
path,
|
||||
`Object parameter does not prove ${required.definitionId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const [name, child] of Object.entries(node)) visit(child, `${path}.${name}`, depth + 1);
|
||||
};
|
||||
visit(template, definition.displayName);
|
||||
const active = new Set<string>();
|
||||
const done = new Set<string>();
|
||||
const checkAlias = (id: string) => {
|
||||
if (done.has(id)) return;
|
||||
if (active.has(id))
|
||||
throw new GenericTypeError("recursive-alias", id, `Recursive value alias: ${[...active, id].join(" -> ")}`);
|
||||
active.add(id);
|
||||
const walk = (node: unknown): void => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
if ("kind" in node && node.kind === "alias")
|
||||
checkAlias((node as ValueAliasDefinition["body"] & { definitionId: string }).definitionId);
|
||||
Object.values(node).forEach(walk);
|
||||
};
|
||||
walk(aliases.get(id)?.body);
|
||||
active.delete(id);
|
||||
done.add(id);
|
||||
};
|
||||
for (const id of aliases.keys()) checkAlias(id);
|
||||
}
|
||||
|
||||
apply(id: string, arguments_: readonly ClosedTypeArgument[]): InterfaceRevision {
|
||||
const definition = this.definitions.get(id);
|
||||
if (!definition) throw new GenericTypeError("unknown-interface", id, "Unknown interface definition");
|
||||
const application = {
|
||||
definitionId: definition.revisionId,
|
||||
source: definition.source,
|
||||
arguments: [...arguments_],
|
||||
...(definition.template?.usesSelf ? { self: this.self } : {}),
|
||||
};
|
||||
const appliedId = definition.template ? appliedInterfaceId(application) : definition.revisionId;
|
||||
const cached = this.applications.get(appliedId);
|
||||
if (cached) return cached;
|
||||
if (this.active.includes(id))
|
||||
throw new GenericTypeError(
|
||||
"recursive-application",
|
||||
id,
|
||||
`Expanding recursive interface application: ${[...this.active, id].join(" -> ")}`,
|
||||
);
|
||||
if (++this.applicationCount > 10000)
|
||||
throw new GenericTypeError("type-complexity-limit", id, "Too many interface applications");
|
||||
this.active.push(id);
|
||||
if (definition.template)
|
||||
this.applications.set(appliedId, {
|
||||
interfaceId: definition.interfaceId,
|
||||
revisionId: appliedId,
|
||||
displayName: definition.displayName,
|
||||
source: definition.source,
|
||||
members: [],
|
||||
application,
|
||||
});
|
||||
try {
|
||||
const obligations: NonNullable<InterfaceRevision["argumentRequirements"]> = [];
|
||||
const instance = instantiateInterface(definition, arguments_, {
|
||||
...this.environment(),
|
||||
implementsInterface: (target, required) => {
|
||||
obligations.push({ target, required });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (instance === definition) return definition;
|
||||
instance.argumentRequirements = obligations;
|
||||
this.applications.set(instance.revisionId, instance);
|
||||
this.definitions.set(instance.revisionId, instance);
|
||||
return instance;
|
||||
} catch (error) {
|
||||
this.applications.delete(appliedId);
|
||||
throw error;
|
||||
} finally {
|
||||
this.active.pop();
|
||||
}
|
||||
}
|
||||
|
||||
interface(context: InterfaceTypeContext): InterfaceApplicationExpression {
|
||||
return this.interfaceByName(context.identifier().getText(), context.typeArguments());
|
||||
}
|
||||
|
||||
interfaceByName(name: string, arguments_: TypeArgumentsContext | null): InterfaceApplicationExpression {
|
||||
const definition = this.interfaces.get(name);
|
||||
if (!definition) throw new GenericTypeError("unknown-interface", name, "Unknown interface");
|
||||
return { definitionId: definition.revisionId, arguments: this.arguments(arguments_) };
|
||||
}
|
||||
|
||||
arguments(context: TypeArgumentsContext | null): TypeArgumentExpression[] {
|
||||
return (context?.typeArgument() ?? []).map((argument): TypeArgumentExpression => {
|
||||
if (argument.INTERFACE())
|
||||
return {
|
||||
kind: "object",
|
||||
target: { kind: "application", application: this.interface(argument.interfaceType()!) },
|
||||
};
|
||||
if (argument.ATOM()) return { kind: "object", target: this.atom(argument.identifier()!.getText()) };
|
||||
if (argument.OBJECT()) return { kind: "object", target: this.objectParameter(argument.identifier()!.getText()) };
|
||||
const type = argument.valueType()!;
|
||||
if (type.getText() === "Self") return { kind: "object", target: { kind: "self" } };
|
||||
const parameter = type.identifier() && this.parameters.get(type.identifier()!.getText());
|
||||
if (parameter?.kind === "object" && !type.typeArguments())
|
||||
return { kind: "object", target: { kind: "parameter", parameterId: parameter.id } };
|
||||
return { kind: "value", type: this.expression(type) };
|
||||
});
|
||||
}
|
||||
|
||||
private atom(name: string): ObjectTypeExpression {
|
||||
if (name === "Self") return { kind: "self" };
|
||||
const atomId = this.atoms.get(name);
|
||||
if (!atomId) throw new GenericTypeError("unknown-atom", name, "Unknown atom");
|
||||
return { kind: "atom", atomId };
|
||||
}
|
||||
|
||||
private objectParameter(name: string): ObjectTypeExpression {
|
||||
if (name === "Self") return { kind: "self" };
|
||||
const parameter = this.parameters.get(name);
|
||||
if (!parameter || parameter.kind !== "object")
|
||||
throw new GenericTypeError("parameter-kind", name, "Expected an object parameter");
|
||||
return { kind: "parameter", parameterId: parameter.id };
|
||||
}
|
||||
|
||||
targetExpression(context: TargetConstraintContext): ObjectTypeExpression {
|
||||
const name = context.identifier().getText();
|
||||
if (context.ATOM()) return this.atom(name);
|
||||
if (context.OBJECT()) return this.objectParameter(name);
|
||||
return { kind: "application", application: this.interfaceByName(name, context.typeArguments()) };
|
||||
}
|
||||
|
||||
expression(context: ValueTypeContext): ValueTypeExpression {
|
||||
if (context.scalarType())
|
||||
return {
|
||||
kind: "scalar",
|
||||
name: context.scalarType()!.getText() as Extract<ValueType, { kind: "scalar" }>["name"],
|
||||
};
|
||||
if (context.UNIT()) return valueType.unit;
|
||||
if (context.WATCH_HANDLE()) return valueType.watchHandle;
|
||||
if (context.MESSAGE()) return valueType.message(literal(context.stringLiteral()!));
|
||||
if (context.OPTIONAL() || context.LIST())
|
||||
return { kind: context.LIST() ? "list" : "optional", value: this.expression(context.valueType()!) };
|
||||
if (context.RECORD()) {
|
||||
const fields = context
|
||||
.recordField()
|
||||
.map((field) => [field.identifier().getText(), this.expression(field.valueType())] as const);
|
||||
if (new Set(fields.map(([name]) => name)).size !== fields.length)
|
||||
throw new GenericTypeError("duplicate-field", "record", "Duplicate record field");
|
||||
return { kind: "record", fields: Object.fromEntries(fields) };
|
||||
}
|
||||
const name = context.identifier()!.getText();
|
||||
if (context.ATOM_REF()) return { kind: "object-ref", expectation: this.atom(name) };
|
||||
if (context.INTERFACE_REF())
|
||||
return {
|
||||
kind: "object-ref",
|
||||
expectation: { kind: "application", application: this.interfaceByName(name, context.typeArguments()) },
|
||||
};
|
||||
if (context.REF()) return { kind: "object-ref", expectation: this.objectParameter(name) };
|
||||
const parameter = this.parameters.get(name);
|
||||
if (parameter) {
|
||||
if (parameter.kind !== "value" || context.typeArguments())
|
||||
throw new GenericTypeError("parameter-kind", name, "Expected a value parameter; object parameters need ref<T>");
|
||||
return { kind: "parameter", parameterId: parameter.id };
|
||||
}
|
||||
if (!this.aliases.has(name)) throw new GenericTypeError("unknown-type", name, "Unknown value type");
|
||||
return { kind: "alias", definitionId: name, arguments: this.arguments(context.typeArguments()) };
|
||||
}
|
||||
|
||||
value(context: ValueTypeContext): ValueType {
|
||||
return new TypeSubstitution(this.environment()).value(this.expression(context));
|
||||
}
|
||||
|
||||
target(context: TargetConstraintContext) {
|
||||
return new TypeSubstitution(this.environment()).object(this.targetExpression(context));
|
||||
}
|
||||
|
||||
declareParameters(context: TypeParametersContext | null, owner: string): TypeParameter[] {
|
||||
const entries = context?.typeParameter() ?? [];
|
||||
const result: TypeParameter[] = entries.map((parameter, index) => {
|
||||
const name = parameter.identifier().getText();
|
||||
if (name === "Self" || this.parameters.has(name))
|
||||
throw new GenericTypeError("duplicate-parameter", owner, `Duplicate or reserved parameter ${name}`);
|
||||
const declaration: TypeParameter = parameter.VALUE()
|
||||
? {
|
||||
id: `${owner}/parameter/${index}`,
|
||||
name,
|
||||
kind: "value",
|
||||
...(parameter.STORABLE() ? { storable: true } : {}),
|
||||
}
|
||||
: { id: `${owner}/parameter/${index}`, name, kind: "object", implements: [] };
|
||||
this.parameters.set(name, declaration);
|
||||
return declaration;
|
||||
});
|
||||
entries.forEach((entry, index) => {
|
||||
const parameter = result[index];
|
||||
if (parameter.kind === "object")
|
||||
parameter.implements = entry.interfaceType().map((bound) => this.interface(bound));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
declareAliases(contexts: readonly TypeAliasDeclContext[]) {
|
||||
for (const context of contexts) {
|
||||
const name = context.identifier().getText();
|
||||
if (this.aliases.has(name)) throw new GenericTypeError("duplicate-alias", name, "Duplicate type alias");
|
||||
this.aliases.set(name, { id: name, parameters: [], body: valueType.unit });
|
||||
}
|
||||
for (const context of contexts) {
|
||||
const name = context.identifier().getText();
|
||||
const previous = this.parameters;
|
||||
this.parameters = new Map();
|
||||
try {
|
||||
this.aliases.set(name, {
|
||||
id: name,
|
||||
parameters: this.declareParameters(context.typeParameters(), JSON.stringify(["alias", name])),
|
||||
body: this.expression(context.valueType()),
|
||||
});
|
||||
} finally {
|
||||
this.parameters = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,15 @@ import {
|
||||
type StateSlotDefinition,
|
||||
type ValueType,
|
||||
type WorkspaceRevision,
|
||||
GenericTypeError,
|
||||
TypeSubstitution,
|
||||
type ValueTypeExpression,
|
||||
type ObjectTypeExpression,
|
||||
type GenericPackageExport,
|
||||
type GenericDependencyPort,
|
||||
specializePackageExport,
|
||||
} from "../capability-model/index.js";
|
||||
import { GenericSourceTypes } from "./generic-types.js";
|
||||
import { QuixosCapabilityLexer } from "./generated/QuixosCapabilityLexer.js";
|
||||
import {
|
||||
QuixosCapabilityParser,
|
||||
@@ -69,6 +77,17 @@ import {
|
||||
|
||||
export type CapabilityResourceKind = "interface" | "package";
|
||||
|
||||
const genericLocations = new WeakMap<GenericTypeError, { line: number; column: number }>();
|
||||
const atGenericSource = <T>(context: ParserRuleContext, lower: () => T): T => {
|
||||
try {
|
||||
return lower();
|
||||
} catch (error) {
|
||||
if (error instanceof GenericTypeError && !genericLocations.has(error))
|
||||
genericLocations.set(error, { line: context.start?.line ?? 0, column: context.start?.column ?? 0 });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export type CapabilityResourceImport = {
|
||||
kind: CapabilityResourceKind;
|
||||
binding: string;
|
||||
@@ -98,6 +117,7 @@ export type CapabilityResource =
|
||||
externalAtoms: AtomDefinition[];
|
||||
externalInterfaces: CapabilityExternalInterface[];
|
||||
revision: InterfaceRevision;
|
||||
specializations?: InterfaceRevision[];
|
||||
}
|
||||
| {
|
||||
kind: "package";
|
||||
@@ -105,6 +125,7 @@ export type CapabilityResource =
|
||||
externalAtoms: AtomDefinition[];
|
||||
externalInterfaces: CapabilityExternalInterface[];
|
||||
revision: PackageRevision;
|
||||
specializations?: InterfaceRevision[];
|
||||
};
|
||||
|
||||
export type CapabilityResourceCompileResult =
|
||||
@@ -137,6 +158,7 @@ export type CapabilitySourceCompileResult =
|
||||
};
|
||||
|
||||
interface InterfaceSymbol {
|
||||
definition?: InterfaceRevision;
|
||||
revisionId: InterfaceRevision["revisionId"];
|
||||
contractAvailable: boolean;
|
||||
members: Map<
|
||||
@@ -154,6 +176,7 @@ interface PackageExportSymbol {
|
||||
}
|
||||
|
||||
interface PackageSymbol {
|
||||
definition?: PackageRevision;
|
||||
revisionId: PackageRevision["revisionId"];
|
||||
exports: Map<string, PackageExportSymbol>;
|
||||
}
|
||||
@@ -164,6 +187,7 @@ interface AttachmentSymbol {
|
||||
}
|
||||
|
||||
interface LoweringState {
|
||||
types: GenericSourceTypes;
|
||||
fileName: string;
|
||||
diagnostics: CapabilitySourceDiagnostic[];
|
||||
atoms: Map<string, AtomId>;
|
||||
@@ -263,6 +287,8 @@ const lowerConstraint = (
|
||||
throw new Error("Missing target constraint after a successful parse");
|
||||
}
|
||||
const name = identifier(context.identifier());
|
||||
if (context.OBJECT() || context.typeArguments() || name === "Self" || state.types.interfaces.get(name)?.template)
|
||||
return state.types.target(context);
|
||||
if (context.ATOM()) {
|
||||
const atomId = requireSymbol(state, state.atoms, name, context, "atom");
|
||||
return atomId ? { kind: "atom", atomId } : undefined;
|
||||
@@ -275,6 +301,16 @@ const lowerValueType = (state: LoweringState, context: ValueTypeContext | null):
|
||||
if (!context) {
|
||||
throw new Error("Missing value type after a successful parse");
|
||||
}
|
||||
if (
|
||||
context.REF() ||
|
||||
context.typeArguments() ||
|
||||
(context.identifier() && !context.ATOM_REF() && !context.INTERFACE_REF())
|
||||
)
|
||||
return state.types.value(context);
|
||||
if (context.INTERFACE_REF() && state.types.interfaces.get(context.identifier()!.getText())?.template)
|
||||
return state.types.value(context);
|
||||
if ((context.ATOM_REF() || context.INTERFACE_REF()) && context.identifier()?.getText() === "Self")
|
||||
return state.types.value(context);
|
||||
const scalar = context.scalarType();
|
||||
if (scalar) {
|
||||
return { kind: "scalar", name: text(scalar) as never };
|
||||
@@ -559,6 +595,188 @@ const lowerInterface = (
|
||||
displayName: alias,
|
||||
source,
|
||||
members,
|
||||
...(context.interfaceType().length
|
||||
? {
|
||||
requiredInterfaces: context
|
||||
.interfaceType()
|
||||
.map((entry) => new TypeSubstitution(state.types.environment()).application(state.types.interface(entry))),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
const hasSelfIdentifier = (context: ParserRuleContext, state: LoweringState): boolean => {
|
||||
if (context.ruleIndex === QuixosCapabilityParser.RULE_identifier && context.getText() === "Self") return true;
|
||||
if (context.ruleIndex === QuixosCapabilityParser.RULE_valueType) {
|
||||
const aliasName = context.children
|
||||
.find((child) => "ruleIndex" in child && child.ruleIndex === QuixosCapabilityParser.RULE_identifier)
|
||||
?.getText();
|
||||
const seen = new Set<string>();
|
||||
const containsSelf = (value: unknown): boolean => {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
if ("kind" in value && value.kind === "self") return true;
|
||||
if ("kind" in value && value.kind === "alias" && "definitionId" in value) {
|
||||
const id = String(value.definitionId);
|
||||
if (!seen.has(id)) {
|
||||
seen.add(id);
|
||||
if (containsSelf(state.types.aliases.get(id)?.body)) return true;
|
||||
}
|
||||
}
|
||||
return Object.values(value).some(containsSelf);
|
||||
};
|
||||
if (aliasName && containsSelf(state.types.aliases.get(aliasName)?.body)) return true;
|
||||
}
|
||||
if (
|
||||
context.ruleIndex === QuixosCapabilityParser.RULE_interfaceType ||
|
||||
((context.ruleIndex === QuixosCapabilityParser.RULE_valueType ||
|
||||
context.ruleIndex === QuixosCapabilityParser.RULE_targetConstraint) &&
|
||||
["interface", "interface-ref"].includes(context.getChild(0)?.getText() ?? ""))
|
||||
) {
|
||||
const name = context.children
|
||||
.find((child) => "ruleIndex" in child && child.ruleIndex === QuixosCapabilityParser.RULE_identifier)
|
||||
?.getText();
|
||||
if (name && state.types.interfaces.get(name)?.template?.usesSelf) return true;
|
||||
}
|
||||
return context.children.some((child) => "ruleIndex" in child && hasSelfIdentifier(child as ParserRuleContext, state));
|
||||
};
|
||||
|
||||
const lowerInterfaceTemplate = (
|
||||
state: LoweringState,
|
||||
context: InterfaceResourceDeclContext,
|
||||
source: SourceRevision,
|
||||
): InterfaceRevision => {
|
||||
const types = state.types;
|
||||
const revisionId = capabilityId.interfaceRevision(stringValue(context.stringLiteral(1)));
|
||||
const parameters = types.declareParameters(context.typeParameters(), JSON.stringify(["interface", revisionId]));
|
||||
const signature = (
|
||||
name: string,
|
||||
id: InterfaceOperation["id"],
|
||||
input: ValueTypeExpression,
|
||||
output: ValueTypeExpression,
|
||||
mode: InterfaceOperationMode = "call",
|
||||
eventType?: ValueTypeExpression,
|
||||
): InterfaceOperation<ValueTypeExpression> => ({
|
||||
id,
|
||||
displayName: name,
|
||||
inputType: input,
|
||||
outputType: output,
|
||||
mode,
|
||||
...(eventType ? { eventType } : {}),
|
||||
});
|
||||
const members: InterfaceMember<ValueTypeExpression, ObjectTypeExpression>[] = context
|
||||
.interfaceMember()
|
||||
.map((entry) => {
|
||||
const value = entry.valueMember();
|
||||
if (value) {
|
||||
const type = types.expression(value.valueType());
|
||||
return {
|
||||
kind: "value",
|
||||
id: capabilityId.member(stringValue(value.stringLiteral())),
|
||||
displayName: identifier(value.identifier()),
|
||||
valueType: type,
|
||||
operations: value.valueMemberOperation().flatMap((operation) => {
|
||||
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
|
||||
if (operation.GET()) return [signature("get", id, valueType.unit, type)];
|
||||
if (operation.SET()) return [signature("set", id, type, valueType.unit)];
|
||||
return [
|
||||
signature("watch-start", id, valueType.unit, valueType.watchHandle, "watch-start", type),
|
||||
signature(
|
||||
"watch-stop",
|
||||
capabilityId.operation(stringValue(operation.stringLiteral(1))),
|
||||
valueType.watchHandle,
|
||||
valueType.unit,
|
||||
"watch-stop",
|
||||
),
|
||||
];
|
||||
}),
|
||||
};
|
||||
}
|
||||
const operation = entry.operationMember();
|
||||
if (operation) {
|
||||
const inputType = types.expression(operation.valueType(0)!);
|
||||
const outputType = types.expression(operation.valueType(1)!);
|
||||
return {
|
||||
kind: "operation",
|
||||
id: capabilityId.member(stringValue(operation.stringLiteral(0))),
|
||||
displayName: identifier(operation.identifier()),
|
||||
inputType,
|
||||
outputType,
|
||||
operations: [
|
||||
signature("call", capabilityId.operation(stringValue(operation.stringLiteral(1))), inputType, outputType),
|
||||
],
|
||||
};
|
||||
}
|
||||
const relationship = entry.relationshipMember()!;
|
||||
const target = types.targetExpression(relationship.targetConstraint());
|
||||
const targetType: ValueTypeExpression = { kind: "object-ref", expectation: target };
|
||||
const cardinality = lowerCardinality(relationship.cardinality());
|
||||
const resolved: ValueTypeExpression =
|
||||
cardinality === "exactly-one"
|
||||
? targetType
|
||||
: { kind: cardinality === "optional-one" ? "optional" : "list", value: targetType };
|
||||
return {
|
||||
kind: "relationship",
|
||||
id: capabilityId.member(stringValue(relationship.stringLiteral())),
|
||||
displayName: identifier(relationship.identifier()),
|
||||
target,
|
||||
cardinality,
|
||||
ordered: Boolean(relationship.ORDERED()),
|
||||
operations: relationship.relationshipOperation().flatMap((operation) => {
|
||||
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
|
||||
if (operation.RESOLVE()) return [signature("resolve", id, valueType.unit, resolved)];
|
||||
if (operation.CONNECT() || operation.DISCONNECT())
|
||||
return [signature(operation.CONNECT() ? "connect" : "disconnect", id, targetType, valueType.unit)];
|
||||
return [
|
||||
signature("watch-start", id, valueType.unit, valueType.watchHandle, "watch-start", resolved),
|
||||
signature(
|
||||
"watch-stop",
|
||||
capabilityId.operation(stringValue(operation.stringLiteral(1))),
|
||||
valueType.watchHandle,
|
||||
valueType.unit,
|
||||
"watch-stop",
|
||||
),
|
||||
];
|
||||
}),
|
||||
};
|
||||
});
|
||||
const requires = context.interfaceType().map((requirement) => types.interface(requirement));
|
||||
const memberIds = new Set<string>();
|
||||
const memberNames = new Set<string>();
|
||||
const operationIds = new Set<string>();
|
||||
for (const member of members) {
|
||||
if (!member.id || memberIds.has(member.id) || memberNames.has(member.displayName))
|
||||
throw new GenericTypeError(
|
||||
"duplicate-interface-member",
|
||||
revisionId,
|
||||
`Invalid or duplicate member ${member.displayName}`,
|
||||
);
|
||||
memberIds.add(member.id);
|
||||
memberNames.add(member.displayName);
|
||||
const operationNames = new Set<string>();
|
||||
for (const operation of member.operations) {
|
||||
if (!operation.id || operationIds.has(operation.id) || operationNames.has(operation.displayName))
|
||||
throw new GenericTypeError(
|
||||
"duplicate-interface-operation",
|
||||
revisionId,
|
||||
`Invalid or duplicate operation ${member.displayName}.${operation.displayName}`,
|
||||
);
|
||||
operationIds.add(operation.id);
|
||||
operationNames.add(operation.displayName);
|
||||
}
|
||||
}
|
||||
const hasSelf = (value: unknown): boolean =>
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
(("kind" in value && value.kind === "self") || Object.values(value).some(hasSelf));
|
||||
const aliases = [...types.aliases.values()];
|
||||
types.parameters = new Map();
|
||||
return {
|
||||
interfaceId: capabilityId.interface(stringValue(context.stringLiteral(0))),
|
||||
revisionId,
|
||||
displayName: identifier(context.identifier()),
|
||||
source,
|
||||
members: [],
|
||||
template: { parameters, members, requires, aliases, usesSelf: hasSelf([parameters, members, requires, aliases]) },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -612,7 +830,12 @@ const lowerDependencyPort = (state: LoweringState, context: DependencyPortContex
|
||||
}
|
||||
const targetName = identifier(context.identifier(1));
|
||||
if (context.INTERFACE()) {
|
||||
const target = requireSymbol(state, state.interfaces, targetName, context, "interface");
|
||||
let target = requireSymbol(state, state.interfaces, targetName, context, "interface");
|
||||
if (target && (context.typeArguments() || target.definition?.template)) {
|
||||
const expression = state.types.interfaceByName(targetName, context.typeArguments());
|
||||
const id = new TypeSubstitution(state.types.environment()).application(expression);
|
||||
target = interfaceSymbolFor(state.types.definitions.get(id)!);
|
||||
}
|
||||
if (target && !target.contractAvailable) {
|
||||
loweringIssue(
|
||||
state,
|
||||
@@ -665,6 +888,12 @@ const lowerReceiver = (
|
||||
state: LoweringState,
|
||||
context: PackageOperationExportContext["receiverRequirement"] extends () => infer Result ? Result : never,
|
||||
): PackageReceiverRequirement => {
|
||||
if (context.OBJECT())
|
||||
throw new GenericTypeError(
|
||||
"unbound-parameter",
|
||||
context.getText(),
|
||||
"An object-parameter receiver requires a generic operation declaration",
|
||||
);
|
||||
if (context.ANY()) {
|
||||
return { kind: "any-object" };
|
||||
}
|
||||
@@ -675,11 +904,12 @@ const lowerReceiver = (
|
||||
atomId: requireSymbol(state, state.atoms, name, context, "atom") ?? capabilityId.atom(`unresolved:${name}`),
|
||||
};
|
||||
}
|
||||
const list = context.identifierList();
|
||||
return {
|
||||
kind: "all-interfaces",
|
||||
interfaceRevisionIds: (list?.identifier() ?? []).map((entry) => {
|
||||
const name = identifier(entry);
|
||||
interfaceRevisionIds: context.interfaceType().map((entry) => {
|
||||
const name = identifier(entry.identifier());
|
||||
if (entry.typeArguments() || state.types.interfaces.get(name)?.template)
|
||||
return new TypeSubstitution(state.types.environment()).application(state.types.interface(entry));
|
||||
return (
|
||||
requireSymbol(state, state.interfaces, name, entry, "interface")?.revisionId ??
|
||||
capabilityId.interfaceRevision(`unresolved:${name}`)
|
||||
@@ -773,22 +1003,148 @@ const lowerPackageConstructor = (
|
||||
return entry;
|
||||
};
|
||||
|
||||
const lowerGenericPackageExport = (
|
||||
state: LoweringState,
|
||||
context: PackageOperationExportContext | PackageFunctionExportContext,
|
||||
): GenericPackageExport => {
|
||||
const previous = state.types.parameters;
|
||||
state.types.parameters = new Map();
|
||||
const id = capabilityId.packageExport(stringValue(context.stringLiteral()));
|
||||
try {
|
||||
const parameters = state.types.declareParameters(context.typeParameters(), JSON.stringify(["export", id]));
|
||||
const dependencyPorts = (context.dependencyBlock()?.dependencyPort() ?? []).map((port): GenericDependencyPort => {
|
||||
const base = {
|
||||
id: capabilityId.dependencyPort(stringValue(port.stringLiteral())),
|
||||
displayName: identifier(port.identifier(0)),
|
||||
};
|
||||
const primitives =
|
||||
port
|
||||
.primitiveList()
|
||||
?.primitive()
|
||||
.map((item) => text(item)) ?? [];
|
||||
if (port.STATE())
|
||||
return {
|
||||
...base,
|
||||
requirement: {
|
||||
kind: "state",
|
||||
valueType: state.types.expression(port.valueType()!),
|
||||
primitives: primitives as StatePrimitive[],
|
||||
},
|
||||
};
|
||||
if (port.EDGE())
|
||||
return {
|
||||
...base,
|
||||
requirement: {
|
||||
kind: "edge",
|
||||
target: state.types.targetExpression(port.targetConstraint()!),
|
||||
cardinality: lowerCardinality(port.cardinality()!),
|
||||
primitives: primitives as EdgePrimitive[],
|
||||
},
|
||||
};
|
||||
if (port.INTERFACE())
|
||||
return {
|
||||
...base,
|
||||
requirement: {
|
||||
kind: "interface",
|
||||
application: state.types.interfaceByName(identifier(port.identifier(1)), port.typeArguments()),
|
||||
},
|
||||
};
|
||||
const name = identifier(port.identifier(1)),
|
||||
parameter = state.types.parameters.get(name);
|
||||
const target: ObjectTypeExpression =
|
||||
parameter?.kind === "object"
|
||||
? { kind: "parameter", parameterId: parameter.id }
|
||||
: { kind: "atom", atomId: requireSymbol(state, state.atoms, name, port, "atom")! };
|
||||
if (!port.valueType())
|
||||
throw new GenericTypeError("missing-constructor-input", id, "Constructor ports require an explicit input type");
|
||||
return {
|
||||
...base,
|
||||
requirement: { kind: "constructor", target, inputType: state.types.expression(port.valueType()!) },
|
||||
};
|
||||
});
|
||||
const operation = "receiverRequirement" in context ? context : undefined;
|
||||
const receiver = operation?.receiverRequirement();
|
||||
let receiverRequirement: GenericPackageExport["receiverRequirement"] = { kind: "any-object" };
|
||||
if (receiver?.ATOM())
|
||||
receiverRequirement = {
|
||||
kind: "target",
|
||||
target: {
|
||||
kind: "atom",
|
||||
atomId: requireSymbol(state, state.atoms, identifier(receiver.identifier()), receiver, "atom")!,
|
||||
},
|
||||
};
|
||||
if (receiver?.OBJECT()) {
|
||||
const name = identifier(receiver.identifier());
|
||||
const parameter = state.types.parameters.get(name);
|
||||
if (parameter?.kind !== "object")
|
||||
throw new GenericTypeError("parameter-kind", id, "Receiver needs an object parameter");
|
||||
receiverRequirement = { kind: "target", target: { kind: "parameter", parameterId: parameter.id } };
|
||||
}
|
||||
if (receiver?.INTERFACES())
|
||||
receiverRequirement = {
|
||||
kind: "interfaces",
|
||||
interfaces: receiver.interfaceType().map((item) => state.types.interface(item)),
|
||||
};
|
||||
const definition: GenericPackageExport = {
|
||||
id,
|
||||
displayName: identifier(context.identifier()),
|
||||
parameters,
|
||||
kind: operation ? "operation" : "function",
|
||||
inputType: state.types.expression(context.valueType(0)!),
|
||||
outputType: state.types.expression(context.valueType(1)!),
|
||||
dependencyPorts,
|
||||
receiverRequirement,
|
||||
...(operation ? { mode: text(operation.operationMode()) as InterfaceOperationMode } : {}),
|
||||
...(operation?.eventClause() ? { eventType: state.types.expression(operation.eventClause()!.valueType()) } : {}),
|
||||
aliases: [...state.types.aliases.values()],
|
||||
};
|
||||
const bindSelf = (value: unknown): unknown => {
|
||||
if (!value || typeof value !== "object") return value;
|
||||
if ("kind" in value && value.kind === "self") {
|
||||
if (receiverRequirement.kind !== "target")
|
||||
throw new GenericTypeError("unbound-self", id, "Generic Self requires an exact or object-parameter receiver");
|
||||
return receiverRequirement.target;
|
||||
}
|
||||
if (Array.isArray(value)) return value.map(bindSelf);
|
||||
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, bindSelf(child)]));
|
||||
};
|
||||
return bindSelf(definition) as GenericPackageExport;
|
||||
} finally {
|
||||
state.types.parameters = previous;
|
||||
}
|
||||
};
|
||||
|
||||
const lowerPackage = (
|
||||
state: LoweringState,
|
||||
context: PackageResourceDeclContext,
|
||||
source: SourceRevision,
|
||||
): PackageRevision => {
|
||||
const alias = identifier(context.identifier());
|
||||
const exports = context.packageExport().map((exportContext) => {
|
||||
const genericExports: GenericPackageExport[] = [];
|
||||
const exports = context.packageExport().flatMap((exportContext): PackageExport[] => {
|
||||
const operation = exportContext.packageOperationExport();
|
||||
if (operation) {
|
||||
return lowerPackageOperation(state, alias, operation);
|
||||
const generic = operation ?? exportContext.packageFunctionExport();
|
||||
if (generic?.typeParameters()) {
|
||||
genericExports.push(atGenericSource(generic, () => lowerGenericPackageExport(state, generic)));
|
||||
return [];
|
||||
}
|
||||
const fn = exportContext.packageFunctionExport();
|
||||
if (fn) {
|
||||
return lowerPackageFunction(state, alias, fn);
|
||||
const constructor = exportContext.packageConstructorExport();
|
||||
const receiver = operation?.receiverRequirement();
|
||||
const selfName = constructor
|
||||
? identifier(constructor.identifier(1))
|
||||
: receiver?.ATOM()
|
||||
? identifier(receiver.identifier())
|
||||
: undefined;
|
||||
const previousSelf = state.types.self;
|
||||
state.types.self = selfName ? state.atoms.get(selfName) : undefined;
|
||||
try {
|
||||
if (operation) return [lowerPackageOperation(state, alias, operation)];
|
||||
const fn = exportContext.packageFunctionExport();
|
||||
if (fn) return [lowerPackageFunction(state, alias, fn)];
|
||||
return [lowerPackageConstructor(state, alias, constructor!)];
|
||||
} finally {
|
||||
state.types.self = previousSelf;
|
||||
}
|
||||
return lowerPackageConstructor(state, alias, exportContext.packageConstructorExport()!);
|
||||
});
|
||||
return {
|
||||
packageId: capabilityId.package(stringValue(context.stringLiteral(0))),
|
||||
@@ -797,6 +1153,7 @@ const lowerPackage = (
|
||||
displayName: alias,
|
||||
source,
|
||||
exports,
|
||||
...(genericExports.length ? { genericExports } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -978,7 +1335,13 @@ const lowerBoundDependencies = (
|
||||
}
|
||||
if (entry.INTERFACE()) {
|
||||
const interfaceName = identifier(entry.identifier(1));
|
||||
const interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, entry, "interface");
|
||||
let interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, entry, "interface");
|
||||
if (interfaceSymbol && (entry.typeArguments() || interfaceSymbol.definition?.template)) {
|
||||
const id = new TypeSubstitution(state.types.environment()).application(
|
||||
state.types.interfaceByName(interfaceName, entry.typeArguments()),
|
||||
);
|
||||
interfaceSymbol = interfaceSymbolFor(state.types.definitions.get(id)!);
|
||||
}
|
||||
const via = entry.VIA() ? traversal(identifier(entry.identifier(2)), identifier(entry.identifier(3))) : undefined;
|
||||
if (entry.VIA() && !via) return [];
|
||||
return interfaceSymbol
|
||||
@@ -1047,10 +1410,23 @@ const lowerConformance = (
|
||||
const atomName = identifier(context.identifier(0));
|
||||
const interfaceName = identifier(context.identifier(1));
|
||||
const atomId = requireSymbol(state, state.atoms, atomName, context, "atom");
|
||||
const interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, context, "interface");
|
||||
let interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, context, "interface");
|
||||
if (!atomId || !interfaceSymbol) {
|
||||
return undefined;
|
||||
}
|
||||
const originalSymbol = interfaceSymbol;
|
||||
if (context.typeArguments() || interfaceSymbol.definition?.template) {
|
||||
const previousSelf = state.types.self;
|
||||
state.types.self = atomId;
|
||||
try {
|
||||
const expression = state.types.interfaceByName(interfaceName, context.typeArguments());
|
||||
const id = new TypeSubstitution(state.types.environment()).application(expression);
|
||||
interfaceSymbol = interfaceSymbolFor(state.types.definitions.get(id)!);
|
||||
state.interfaces.set(interfaceName, interfaceSymbol);
|
||||
} finally {
|
||||
state.types.self = previousSelf;
|
||||
}
|
||||
}
|
||||
const operationBindings = context
|
||||
.conformanceItem()
|
||||
.flatMap<WorkspaceRevision["conformances"][number]["operationBindings"][number]>((item) => {
|
||||
@@ -1119,9 +1495,30 @@ const lowerConformance = (
|
||||
const packageName = identifier(provider.identifier(0));
|
||||
const exportName = identifier(provider.identifier(1));
|
||||
const packageSymbol = requireSymbol(state, state.packages, packageName, provider, "package");
|
||||
const exportSymbol = packageSymbol
|
||||
let exportSymbol = packageSymbol
|
||||
? requireSymbol(state, packageSymbol.exports, exportName, provider, `export on ${packageName}`)
|
||||
: undefined;
|
||||
const generic = packageSymbol?.definition?.genericExports?.find((entry) => entry.displayName === exportName);
|
||||
if (packageSymbol?.definition && generic) {
|
||||
const arguments_ = state.types
|
||||
.arguments(provider.typeArguments())
|
||||
.map((entry) => new TypeSubstitution({ ...state.types.environment(), self: atomId }).argument(entry));
|
||||
const pkg = packageSymbol.definition;
|
||||
const specialized = specializePackageExport(pkg, generic, arguments_, {
|
||||
...state.types.environment(),
|
||||
implementsInterface: (target, required) => {
|
||||
(pkg.argumentRequirements ??= []).push({ target, required });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (!pkg.exports.some((entry) => entry.id === specialized.id)) pkg.exports.push(specialized);
|
||||
exportSymbol = {
|
||||
exportId: specialized.id,
|
||||
ports: new Map(specialized.dependencyPorts.map((entry) => [entry.displayName, entry.id])),
|
||||
};
|
||||
} else if (provider.typeArguments()) {
|
||||
throw new GenericTypeError("type-arity", exportName, "Non-generic export takes no type arguments");
|
||||
}
|
||||
return packageSymbol && exportSymbol
|
||||
? [
|
||||
{
|
||||
@@ -1146,6 +1543,7 @@ const lowerConformance = (
|
||||
const lowered = lowerRelationshipMaterialization(state, interfaceName, materialization);
|
||||
return lowered ? [lowered] : [];
|
||||
});
|
||||
state.interfaces.set(interfaceName, originalSymbol);
|
||||
return {
|
||||
atomId,
|
||||
interfaceRevisionId: interfaceSymbol.revisionId,
|
||||
@@ -1158,6 +1556,7 @@ const lowerConformance = (
|
||||
};
|
||||
|
||||
const interfaceSymbolFor = (revision: InterfaceRevision): InterfaceSymbol => ({
|
||||
definition: revision,
|
||||
revisionId: revision.revisionId,
|
||||
contractAvailable: true,
|
||||
members: new Map(
|
||||
@@ -1172,9 +1571,10 @@ const interfaceSymbolFor = (revision: InterfaceRevision): InterfaceSymbol => ({
|
||||
});
|
||||
|
||||
const packageSymbolFor = (revision: PackageRevision): PackageSymbol => ({
|
||||
definition: structuredClone(revision),
|
||||
revisionId: revision.revisionId,
|
||||
exports: new Map(
|
||||
revision.exports.map((entry) => [
|
||||
[...revision.exports, ...(revision.genericExports ?? [])].map((entry) => [
|
||||
entry.displayName,
|
||||
{
|
||||
exportId: entry.id,
|
||||
@@ -1193,8 +1593,10 @@ const registerImports = (
|
||||
state: LoweringState,
|
||||
contexts: readonly ResourceImportDeclContext[],
|
||||
environment: CapabilityImportEnvironment,
|
||||
) =>
|
||||
contexts.map((context) => {
|
||||
) => {
|
||||
for (const definition of environment.interfaceClosure ?? [])
|
||||
state.types.definitions.set(definition.revisionId, definition);
|
||||
return contexts.map((context) => {
|
||||
const imported = resourceImport(context);
|
||||
if (imported.kind === "interface") {
|
||||
const revision = environment.interfaces?.get(imported.binding);
|
||||
@@ -1207,6 +1609,7 @@ const registerImports = (
|
||||
);
|
||||
} else {
|
||||
declareSymbol(state, state.interfaces, imported.binding, interfaceSymbolFor(revision), context, "interface");
|
||||
state.types.register(imported.binding, revision);
|
||||
}
|
||||
} else {
|
||||
const revision = environment.packages?.get(imported.binding);
|
||||
@@ -1223,6 +1626,7 @@ const registerImports = (
|
||||
}
|
||||
return imported;
|
||||
});
|
||||
};
|
||||
|
||||
const uniqueExactRevisions = <
|
||||
Revision extends {
|
||||
@@ -1295,6 +1699,8 @@ const lowerWorkspace = (
|
||||
: [];
|
||||
});
|
||||
|
||||
state.types.declareAliases(items.flatMap((item) => item.typeAliasDecl() ?? []));
|
||||
|
||||
const interfaceImports = uniqueExactRevisions([
|
||||
...(environment.interfaceClosure ?? []),
|
||||
...[...(environment.interfaces?.values() ?? [])],
|
||||
@@ -1319,6 +1725,7 @@ const lowerWorkspace = (
|
||||
}
|
||||
const conformance = item.conformanceDecl();
|
||||
if (conformance) {
|
||||
state.types.self = state.atoms.get(identifier(conformance.identifier(0)));
|
||||
const attachments = conformance.conformanceItem().flatMap((entry) => {
|
||||
const declaration = entry.attachmentDecl();
|
||||
if (!declaration) {
|
||||
@@ -1332,6 +1739,7 @@ const lowerWorkspace = (
|
||||
return [attachment];
|
||||
});
|
||||
privateAttachments.set(conformance, attachments);
|
||||
state.types.self = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1340,7 +1748,9 @@ const lowerWorkspace = (
|
||||
if (!context) {
|
||||
return [];
|
||||
}
|
||||
const conformance = lowerConformance(state, context, privateAttachments.get(context) ?? []);
|
||||
const conformance = atGenericSource(context, () =>
|
||||
lowerConformance(state, context, privateAttachments.get(context) ?? []),
|
||||
);
|
||||
return conformance ? [conformance] : [];
|
||||
});
|
||||
|
||||
@@ -1382,8 +1792,15 @@ const lowerWorkspace = (
|
||||
sourceRootCommit: stringValue(context.stringLiteral(2)),
|
||||
atoms,
|
||||
sharedAttachments,
|
||||
interfaceImports,
|
||||
packageImports,
|
||||
interfaceImports: uniqueExactRevisions([
|
||||
...interfaceImports.filter((entry) => !entry.template),
|
||||
...state.types.applications.values(),
|
||||
]),
|
||||
packageImports: packageImports.map(
|
||||
(revision) =>
|
||||
[...state.packages.values()].find((entry) => entry.revisionId === revision.revisionId)?.definition ??
|
||||
revision,
|
||||
),
|
||||
conformances,
|
||||
constructors,
|
||||
},
|
||||
@@ -1408,14 +1825,18 @@ export const parseDocument = (
|
||||
return { tree, tokens, diagnostics };
|
||||
};
|
||||
|
||||
const newLoweringState = (fileName: string, diagnostics: CapabilitySourceDiagnostic[]): LoweringState => ({
|
||||
fileName,
|
||||
diagnostics,
|
||||
atoms: new Map(),
|
||||
interfaces: new Map(),
|
||||
packages: new Map(),
|
||||
attachments: new Map(),
|
||||
});
|
||||
const newLoweringState = (fileName: string, diagnostics: CapabilitySourceDiagnostic[]): LoweringState => {
|
||||
const atoms = new Map<string, AtomId>();
|
||||
return {
|
||||
fileName,
|
||||
diagnostics,
|
||||
atoms,
|
||||
types: new GenericSourceTypes(atoms),
|
||||
interfaces: new Map(),
|
||||
packages: new Map(),
|
||||
attachments: new Map(),
|
||||
};
|
||||
};
|
||||
|
||||
const validationDiagnostics = (
|
||||
fileName: string,
|
||||
@@ -1471,6 +1892,7 @@ const resourcePreambleParts = (contexts: readonly ResourcePreambleContext[]) =>
|
||||
imports: contexts.flatMap((context) => context.resourceImportDecl() ?? []),
|
||||
atoms: contexts.flatMap((context) => context.externalAtomDecl() ?? []),
|
||||
interfaces: contexts.flatMap((context) => context.externalInterfaceDecl() ?? []),
|
||||
aliases: contexts.flatMap((context) => context.typeAliasDecl() ?? []),
|
||||
});
|
||||
|
||||
const resourceValidationWorkspace = (
|
||||
@@ -1493,6 +1915,7 @@ const resourceValidationWorkspace = (
|
||||
...(environment.interfaceClosure ?? []),
|
||||
...[...(environment.interfaces?.values() ?? [])],
|
||||
...(resource.kind === "interface" ? [resource.revision] : []),
|
||||
...(resource.specializations ?? []),
|
||||
...resource.externalInterfaces
|
||||
.filter((requirement) => !resolvedInterfaceIds.has(requirement.revisionId))
|
||||
.map(
|
||||
@@ -1507,7 +1930,9 @@ const resourceValidationWorkspace = (
|
||||
members: [],
|
||||
}),
|
||||
),
|
||||
]),
|
||||
])
|
||||
.filter((entry) => !entry.template)
|
||||
.map((entry) => ({ ...entry, argumentRequirements: [] })),
|
||||
packageImports: uniqueExactRevisions([
|
||||
...(environment.packageClosure ?? []),
|
||||
...[...(environment.packages?.values() ?? [])],
|
||||
@@ -1518,7 +1943,7 @@ const resourceValidationWorkspace = (
|
||||
};
|
||||
};
|
||||
|
||||
export const compileCapabilityResourceSource = (
|
||||
const compileCapabilityResourceSourceInternal = (
|
||||
sourceText: string,
|
||||
options: {
|
||||
source: SourceRevision;
|
||||
@@ -1556,10 +1981,18 @@ export const compileCapabilityResourceSource = (
|
||||
const externalAtoms = externalAtomsFrom(state, preamble.atoms);
|
||||
const imports = registerImports(state, preamble.imports, environment);
|
||||
const externalInterfaces = externalInterfacesFrom(state, preamble.interfaces);
|
||||
state.types.declareAliases(preamble.aliases);
|
||||
|
||||
let resource: CapabilityResource;
|
||||
if (interfaceContext) {
|
||||
const alias = identifier(interfaceContext.identifier());
|
||||
state.types.register(alias, {
|
||||
interfaceId: capabilityId.interface(stringValue(interfaceContext.stringLiteral(0))),
|
||||
revisionId: capabilityId.interfaceRevision(stringValue(interfaceContext.stringLiteral(1))),
|
||||
displayName: alias,
|
||||
source: options.source,
|
||||
members: [],
|
||||
});
|
||||
declareSymbol(
|
||||
state,
|
||||
state.interfaces,
|
||||
@@ -1577,7 +2010,10 @@ export const compileCapabilityResourceSource = (
|
||||
imports,
|
||||
externalAtoms,
|
||||
externalInterfaces,
|
||||
revision: lowerInterface(state, interfaceContext, options.source),
|
||||
revision:
|
||||
interfaceContext.typeParameters() || hasSelfIdentifier(interfaceContext, state)
|
||||
? atGenericSource(interfaceContext, () => lowerInterfaceTemplate(state, interfaceContext, options.source))
|
||||
: lowerInterface(state, interfaceContext, options.source),
|
||||
};
|
||||
} else {
|
||||
const context = packageContext!;
|
||||
@@ -1602,6 +2038,77 @@ export const compileCapabilityResourceSource = (
|
||||
};
|
||||
}
|
||||
|
||||
if (resource.kind === "interface") {
|
||||
state.types.register(identifier(interfaceContext!.identifier()), resource.revision);
|
||||
atGenericSource(interfaceContext!, () => state.types.validateTemplate(resource.revision));
|
||||
} else {
|
||||
const names = new Set(resource.revision.exports.map((entry) => entry.displayName));
|
||||
const ids = new Set(resource.revision.exports.map((entry) => entry.id));
|
||||
for (const entry of resource.revision.genericExports ?? []) {
|
||||
if (names.has(entry.displayName) || ids.has(entry.id))
|
||||
throw new GenericTypeError("duplicate-package-export", entry.displayName, "Duplicate generic export");
|
||||
names.add(entry.displayName);
|
||||
ids.add(entry.id);
|
||||
if (
|
||||
entry.kind === "operation" &&
|
||||
(entry.mode === "watch-start" || entry.mode === "subscribe") !== Boolean(entry.eventType)
|
||||
)
|
||||
throw new GenericTypeError(
|
||||
"invalid-operation",
|
||||
entry.displayName,
|
||||
"Only watch-start/subscribe operations must declare an event type",
|
||||
);
|
||||
const portIds = new Set<string>(),
|
||||
portNames = new Set<string>();
|
||||
const types: ValueTypeExpression[] = [
|
||||
entry.inputType,
|
||||
entry.outputType,
|
||||
...(entry.eventType ? [entry.eventType] : []),
|
||||
];
|
||||
const requires = entry.parameters.flatMap((parameter) =>
|
||||
parameter.kind === "object" ? parameter.implements : [],
|
||||
);
|
||||
for (const port of entry.dependencyPorts) {
|
||||
if (portIds.has(port.id) || portNames.has(port.displayName))
|
||||
throw new GenericTypeError("duplicate-dependency-port", entry.displayName, "Duplicate port");
|
||||
portIds.add(port.id);
|
||||
portNames.add(port.displayName);
|
||||
const requirement = port.requirement;
|
||||
if (requirement.kind === "state") {
|
||||
if (requirement.primitives.some((p) => !["read", "write", "watch-start", "watch-stop"].includes(p)))
|
||||
throw new GenericTypeError("invalid-port-primitive", port.displayName, "Invalid state primitive");
|
||||
types.push(requirement.valueType);
|
||||
}
|
||||
if (requirement.kind === "edge") types.push({ kind: "object-ref", expectation: requirement.target });
|
||||
if (requirement.kind === "constructor")
|
||||
types.push(requirement.inputType, { kind: "object-ref", expectation: requirement.target });
|
||||
if (requirement.kind === "interface") requires.push(requirement.application);
|
||||
}
|
||||
if (entry.receiverRequirement.kind === "interfaces") requires.push(...entry.receiverRequirement.interfaces);
|
||||
state.types.validateTemplate({
|
||||
interfaceId: capabilityId.interface(entry.id),
|
||||
revisionId: capabilityId.interfaceRevision(entry.id),
|
||||
displayName: entry.displayName,
|
||||
source: resource.revision.source,
|
||||
members: [],
|
||||
template: {
|
||||
parameters: entry.parameters,
|
||||
aliases: entry.aliases,
|
||||
usesSelf: false,
|
||||
requires,
|
||||
members: types.map((type, index) => ({
|
||||
kind: "value",
|
||||
id: capabilityId.member(String(index)),
|
||||
displayName: String(index),
|
||||
valueType: type,
|
||||
operations: [],
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
resource.specializations = [...state.types.applications.values()];
|
||||
|
||||
if (diagnostics.length > 0) {
|
||||
return { ok: false, diagnostics };
|
||||
}
|
||||
@@ -1615,7 +2122,7 @@ export const compileCapabilityResourceSource = (
|
||||
return { ok: true, resource, diagnostics: [] };
|
||||
};
|
||||
|
||||
export const compileCapabilitySource = (
|
||||
const compileCapabilitySourceInternal = (
|
||||
source: string,
|
||||
fileName = "<memory>",
|
||||
environment: CapabilityImportEnvironment = {},
|
||||
@@ -1666,3 +2173,35 @@ export const compileCapabilitySource = (
|
||||
diagnostics: [],
|
||||
};
|
||||
};
|
||||
|
||||
const genericDiagnostic = (error: GenericTypeError, fileName: string): CapabilitySourceDiagnostic => ({
|
||||
phase: "lowering",
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
fileName,
|
||||
line: genericLocations.get(error)?.line ?? 0,
|
||||
column: genericLocations.get(error)?.column ?? 0,
|
||||
path: error.path,
|
||||
});
|
||||
|
||||
export const compileCapabilityResourceSource = (
|
||||
...args: Parameters<typeof compileCapabilityResourceSourceInternal>
|
||||
): CapabilityResourceCompileResult => {
|
||||
try {
|
||||
return compileCapabilityResourceSourceInternal(...args);
|
||||
} catch (error) {
|
||||
if (!(error instanceof GenericTypeError)) throw error;
|
||||
return { ok: false, diagnostics: [genericDiagnostic(error, args[1].fileName ?? "<memory>")] };
|
||||
}
|
||||
};
|
||||
|
||||
export const compileCapabilitySource = (
|
||||
...args: Parameters<typeof compileCapabilitySourceInternal>
|
||||
): CapabilitySourceCompileResult => {
|
||||
try {
|
||||
return compileCapabilitySourceInternal(...args);
|
||||
} catch (error) {
|
||||
if (!(error instanceof GenericTypeError)) throw error;
|
||||
return { ok: false, diagnostics: [genericDiagnostic(error, args[1] ?? "<memory>")] };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ type Change = { file: string; before: string | null; after: string; mode: number
|
||||
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
|
||||
const safeFile = (file: string) => {
|
||||
if (
|
||||
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|lock|ts|tsx|css|mjs|json|nix|txtpb))$/.test(
|
||||
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
|
||||
file,
|
||||
) ||
|
||||
file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readFile, writeFile } from "node:fs/promises";
|
||||
import process from "node:process";
|
||||
import { compileWorkspaceRepository } from "./assembly.js";
|
||||
import { createGitCapabilityResolver } from "./git-resolver.js";
|
||||
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
|
||||
import {
|
||||
planEvolution,
|
||||
runtimeContracts,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY
|
||||
[--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID]
|
||||
[--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH]
|
||||
[--reviews REVIEW_JSON]
|
||||
[--reviews REVIEW_JSON] [--schemas-out PATH]
|
||||
|
||||
Resolves a workspace's recursive resource-lock graph, clones every exact
|
||||
resource revision, validates standalone interface/package manifests, and emits
|
||||
@@ -35,6 +36,7 @@ const parseArgs = (args: string[]) => {
|
||||
rootDirectory,
|
||||
checkoutRoot,
|
||||
graphOut: values.get("--graph-out"),
|
||||
schemasOut: values.get("--schemas-out"),
|
||||
snapshotMap: values.get("--snapshot-map"),
|
||||
workspaceId: values.get("--workspace-id"),
|
||||
workspaceRevisionId: values.get("--workspace-revision-id"),
|
||||
@@ -96,6 +98,24 @@ const main = async () => {
|
||||
);
|
||||
}
|
||||
const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) };
|
||||
if (options.schemasOut) {
|
||||
const schemas: Record<string, unknown> = {};
|
||||
for (const node of assembled.resources.filter((entry) => entry.kind === "package")) {
|
||||
const closure = new Map<string, typeof node>();
|
||||
const visit = (entry: typeof node) => {
|
||||
if (closure.has(entry.key)) return;
|
||||
closure.set(entry.key, entry);
|
||||
entry.dependencies.forEach(visit);
|
||||
};
|
||||
visit(node);
|
||||
schemas[node.resource.revision.revisionId] = specializeBindingSchema(
|
||||
bindingSchema({ resources: [...closure.values()] }),
|
||||
candidate,
|
||||
node.resource.revision.revisionId,
|
||||
);
|
||||
}
|
||||
await writeFile(options.schemasOut, JSON.stringify(schemas));
|
||||
}
|
||||
if (options.evolutionOut) {
|
||||
const baseline = options.baseline
|
||||
? (JSON.parse(await readFile(options.baseline, "utf8")) as WorkspaceRevision)
|
||||
|
||||
@@ -184,6 +184,9 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
||||
materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId),
|
||||
});
|
||||
parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`);
|
||||
const contract = workspace.interfaceImports.find((entry) => entry.revisionId === conformance.interfaceRevisionId);
|
||||
for (const required of contract?.requiredInterfaces ?? [])
|
||||
parent.dependencies.add(conformanceKey(conformance.atomId, required));
|
||||
for (const attachment of conformance.privateAttachments) parent.dependencies.add(`attachment:${attachment.id}`);
|
||||
for (const operation of conformance.operationBindings)
|
||||
binding(parent, operation.binding, conformance.atomId, {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
capabilityId,
|
||||
type PackageExport,
|
||||
type PackageExportId,
|
||||
type DependencyPort,
|
||||
type PackageRevision,
|
||||
} from "./types.js";
|
||||
import {
|
||||
TypeSubstitution,
|
||||
GenericTypeError,
|
||||
bindTypeParameters,
|
||||
canonicalTypeArgument,
|
||||
type TypeParameter,
|
||||
type ValueTypeExpression,
|
||||
type ObjectTypeExpression,
|
||||
type InterfaceApplicationExpression,
|
||||
type GenericTypeEnvironment,
|
||||
type ClosedTypeArgument,
|
||||
type ValueAliasDefinition,
|
||||
} from "./generics.js";
|
||||
|
||||
export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
|
||||
requirement:
|
||||
| {
|
||||
kind: "state";
|
||||
valueType: ValueTypeExpression;
|
||||
primitives: Extract<DependencyPort["requirement"], { kind: "state" }>["primitives"];
|
||||
}
|
||||
| {
|
||||
kind: "edge";
|
||||
target: ObjectTypeExpression;
|
||||
cardinality: Extract<DependencyPort["requirement"], { kind: "edge" }>["cardinality"];
|
||||
primitives: Extract<DependencyPort["requirement"], { kind: "edge" }>["primitives"];
|
||||
}
|
||||
| { kind: "interface"; application: InterfaceApplicationExpression }
|
||||
| { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression };
|
||||
};
|
||||
|
||||
export interface GenericPackageExport {
|
||||
id: PackageExportId;
|
||||
displayName: string;
|
||||
parameters: TypeParameter[];
|
||||
kind: "operation" | "function";
|
||||
inputType: ValueTypeExpression;
|
||||
outputType: ValueTypeExpression;
|
||||
eventType?: ValueTypeExpression;
|
||||
mode?: Extract<PackageExport, { kind: "operation" }>["mode"];
|
||||
receiverRequirement:
|
||||
| { kind: "any-object" }
|
||||
| { kind: "target"; target: ObjectTypeExpression }
|
||||
| { kind: "interfaces"; interfaces: InterfaceApplicationExpression[] };
|
||||
dependencyPorts: GenericDependencyPort[];
|
||||
aliases: ValueAliasDefinition[];
|
||||
}
|
||||
|
||||
/** A closed manifest is a Nix build input, never a mutable source checkout. */
|
||||
export const specializePackageExport = (
|
||||
pkg: PackageRevision,
|
||||
definition: GenericPackageExport,
|
||||
arguments_: ClosedTypeArgument[],
|
||||
environment: GenericTypeEnvironment,
|
||||
): PackageExport => {
|
||||
const lexical = { ...environment, aliases: new Map(definition.aliases.map((alias) => [alias.id, alias])) };
|
||||
const bindings = bindTypeParameters(definition.parameters, arguments_, lexical, definition.displayName);
|
||||
const substitution = new TypeSubstitution({ ...lexical, arguments: bindings });
|
||||
const digest = createHash("sha256")
|
||||
.update(
|
||||
JSON.stringify([
|
||||
"quixos-package-specialization-v1",
|
||||
pkg.revisionId,
|
||||
[pkg.source.repository, pkg.source.commit],
|
||||
definition.id,
|
||||
arguments_.map(canonicalTypeArgument),
|
||||
environment.self ?? null,
|
||||
]),
|
||||
)
|
||||
.digest("hex");
|
||||
const ports: DependencyPort[] = definition.dependencyPorts.map((port) => {
|
||||
const requirement = port.requirement;
|
||||
switch (requirement.kind) {
|
||||
case "state":
|
||||
return { ...port, requirement: { ...requirement, valueType: substitution.value(requirement.valueType) } };
|
||||
case "edge":
|
||||
return { ...port, requirement: { ...requirement, target: substitution.object(requirement.target) } };
|
||||
case "interface":
|
||||
return {
|
||||
...port,
|
||||
requirement: { kind: "interface", interfaceRevisionId: substitution.application(requirement.application) },
|
||||
};
|
||||
case "constructor": {
|
||||
const target = substitution.object(requirement.target);
|
||||
if (target.kind !== "atom")
|
||||
throw new GenericTypeError(
|
||||
"constructor-target",
|
||||
port.displayName,
|
||||
"Constructor ports require a concrete atom argument, not an interface view",
|
||||
);
|
||||
return {
|
||||
...port,
|
||||
requirement: {
|
||||
kind: "constructor",
|
||||
atomId: target.atomId,
|
||||
inputType: substitution.value(requirement.inputType),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
const common = {
|
||||
id: capabilityId.packageExport(`export-application:sha256:${digest}`),
|
||||
displayName: `${definition.displayName}$${digest}`,
|
||||
inputType: substitution.value(definition.inputType),
|
||||
outputType: substitution.value(definition.outputType),
|
||||
dependencyPorts: ports,
|
||||
application: {
|
||||
exportId: definition.id,
|
||||
arguments: structuredClone(arguments_),
|
||||
...(environment.self ? { self: environment.self } : {}),
|
||||
},
|
||||
};
|
||||
if (definition.kind === "function") return { ...common, kind: "function" };
|
||||
const receiver = definition.receiverRequirement;
|
||||
const target = receiver.kind === "target" ? substitution.object(receiver.target) : undefined;
|
||||
return {
|
||||
...common,
|
||||
kind: "operation",
|
||||
mode: definition.mode!,
|
||||
...(definition.eventType ? { eventType: substitution.value(definition.eventType) } : {}),
|
||||
receiverRequirement: target
|
||||
? target.kind === "atom"
|
||||
? { kind: "exact-atom", atomId: target.atomId }
|
||||
: { kind: "all-interfaces", interfaceRevisionIds: [target.interfaceRevisionId] }
|
||||
: receiver.kind === "interfaces"
|
||||
? {
|
||||
kind: "all-interfaces",
|
||||
interfaceRevisionIds: receiver.interfaces.map((value) => substitution.application(value)),
|
||||
}
|
||||
: { kind: "any-object" },
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,420 @@
|
||||
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<ValueType, { kind: "builtin" | "scalar" | "message" }>
|
||||
| { kind: "parameter"; parameterId: string }
|
||||
| { kind: "record"; fields: Record<string, ValueTypeExpression> }
|
||||
| { 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<ValueTypeExpression, ObjectTypeExpression>[];
|
||||
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<ValueTypeExpression>): 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", valueType: substitution.value(member.valueType, member.displayName) };
|
||||
case "relationship":
|
||||
return {
|
||||
...common,
|
||||
kind: "relationship",
|
||||
target: substitution.object(member.target, member.displayName),
|
||||
cardinality: member.cardinality,
|
||||
ordered: member.ordered,
|
||||
};
|
||||
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<string, ClosedTypeArgument>;
|
||||
self?: AtomId;
|
||||
aliases?: ReadonlyMap<string, ValueAliasDefinition>;
|
||||
/** 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<T>`);
|
||||
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<string, ClosedTypeArgument> => {
|
||||
if (parameters.length !== arguments_.length)
|
||||
fail("type-arity", path, `Expected ${parameters.length} type arguments, received ${arguments_.length}`);
|
||||
const bindings = new Map<string, ClosedTypeArgument>();
|
||||
const names = new Set<string>();
|
||||
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;
|
||||
};
|
||||
@@ -2,3 +2,5 @@ export * from "./types.js";
|
||||
export * from "./validation.js";
|
||||
export * from "./evolution.js";
|
||||
export * from "./migrations.js";
|
||||
export * from "./generics.js";
|
||||
export * from "./generic-packages.js";
|
||||
|
||||
@@ -104,25 +104,25 @@ export interface AtomDefinition {
|
||||
|
||||
export type InterfaceOperationMode = "call" | "watch-start" | "watch-stop" | "subscribe" | "unsubscribe";
|
||||
|
||||
export interface InterfaceOperation {
|
||||
export interface InterfaceOperation<Type = ValueType> {
|
||||
id: OperationId;
|
||||
displayName: string;
|
||||
inputType: ValueType;
|
||||
outputType: ValueType;
|
||||
inputType: Type;
|
||||
outputType: Type;
|
||||
mode: InterfaceOperationMode;
|
||||
/** Required for watch-start/subscribe and absent for other modes. */
|
||||
eventType?: ValueType;
|
||||
eventType?: Type;
|
||||
}
|
||||
|
||||
interface InterfaceMemberBase {
|
||||
interface InterfaceMemberBase<Type = ValueType> {
|
||||
id: MemberId;
|
||||
displayName: string;
|
||||
operations: InterfaceOperation[];
|
||||
operations: InterfaceOperation<Type>[];
|
||||
}
|
||||
|
||||
export interface ValueInterfaceMember extends InterfaceMemberBase {
|
||||
export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
|
||||
kind: "value";
|
||||
valueType: ValueType;
|
||||
valueType: Type;
|
||||
}
|
||||
|
||||
export type EdgeCardinality = "optional-one" | "exactly-one" | "many" | "many-unique";
|
||||
@@ -134,21 +134,27 @@ export type EdgeEndpointConstraint =
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
};
|
||||
|
||||
export interface RelationshipInterfaceMember extends InterfaceMemberBase {
|
||||
export interface RelationshipInterfaceMember<
|
||||
Type = ValueType,
|
||||
Target = EdgeEndpointConstraint,
|
||||
> extends InterfaceMemberBase<Type> {
|
||||
kind: "relationship";
|
||||
target: EdgeEndpointConstraint;
|
||||
target: Target;
|
||||
cardinality: EdgeCardinality;
|
||||
ordered: boolean;
|
||||
}
|
||||
|
||||
/** A named callable capability that is not value or relationship sugar. */
|
||||
export interface OperationInterfaceMember extends InterfaceMemberBase {
|
||||
export interface OperationInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
|
||||
kind: "operation";
|
||||
inputType: ValueType;
|
||||
outputType: ValueType;
|
||||
inputType: Type;
|
||||
outputType: Type;
|
||||
}
|
||||
|
||||
export type InterfaceMember = ValueInterfaceMember | RelationshipInterfaceMember | OperationInterfaceMember;
|
||||
export type InterfaceMember<Type = ValueType, Target = EdgeEndpointConstraint> =
|
||||
| ValueInterfaceMember<Type>
|
||||
| RelationshipInterfaceMember<Type, Target>
|
||||
| OperationInterfaceMember<Type>;
|
||||
|
||||
export interface InterfaceRevision {
|
||||
interfaceId: InterfaceId;
|
||||
@@ -156,6 +162,12 @@ export interface InterfaceRevision {
|
||||
displayName: string;
|
||||
source: SourceRevision;
|
||||
members: InterfaceMember[];
|
||||
/** Authored generic definition; never an executable contract by itself. */
|
||||
template?: import("./generics.js").GenericInterfaceTemplate;
|
||||
/** Immutable provenance of a closed generic application. */
|
||||
application?: import("./generics.js").AppliedInterfaceIdentity;
|
||||
requiredInterfaces?: InterfaceRevisionId[];
|
||||
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
|
||||
}
|
||||
|
||||
export type StoragePolicy = { kind: "optimistic-register" } | { kind: "crdt-document"; updateType: ValueType };
|
||||
@@ -233,6 +245,8 @@ interface PackageExportBase {
|
||||
inputType: ValueType;
|
||||
outputType: ValueType;
|
||||
dependencyPorts: DependencyPort[];
|
||||
/** Closed adapter served by the same immutable package build. */
|
||||
application?: { exportId: PackageExportId; arguments: import("./generics.js").ClosedTypeArgument[]; self?: AtomId };
|
||||
}
|
||||
|
||||
export interface PackageOperationExport extends PackageExportBase {
|
||||
@@ -254,6 +268,8 @@ export interface PackageConstructorExport extends PackageExportBase {
|
||||
export type PackageExport = PackageOperationExport | PackageFunctionExport | PackageConstructorExport;
|
||||
|
||||
export interface PackageRevision {
|
||||
genericExports?: import("./generic-packages.js").GenericPackageExport[];
|
||||
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
|
||||
migrationCatalog?: import("./migrations.js").MigrationCatalog;
|
||||
packageId: PackageId;
|
||||
revisionId: PackageRevisionId;
|
||||
|
||||
@@ -34,6 +34,9 @@ import type {
|
||||
WorkspaceRevision,
|
||||
} from "./types.js";
|
||||
import { valueType } from "./types.js";
|
||||
import { appliedInterfaceId, GenericTypeError } from "./generics.js";
|
||||
import { specializePackageExport } from "./generic-packages.js";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
|
||||
export type CapabilityValidationIssueCode =
|
||||
| "invalid-semantic-major"
|
||||
@@ -217,7 +220,12 @@ const validateValueType = (
|
||||
}
|
||||
return;
|
||||
case "builtin":
|
||||
if (!["unit", "watch-handle"].includes(type.name))
|
||||
issue(issues, "invalid-value-type", path, "Unknown builtin value type");
|
||||
return;
|
||||
case "scalar":
|
||||
if (!["bool", "bytes", "double", "int32", "int64", "string", "uint32", "uint64"].includes(type.name))
|
||||
issue(issues, "invalid-value-type", path, "Unknown scalar value type");
|
||||
return;
|
||||
case "message":
|
||||
requireText(issues, type.descriptorId, `${path}.descriptorId`, "Message descriptor identity");
|
||||
@@ -244,6 +252,14 @@ const validateValueType = (
|
||||
`Unknown interface revision ${type.expectation.interfaceRevisionId}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
issue(
|
||||
issues,
|
||||
"invalid-value-type",
|
||||
path,
|
||||
"Installed contracts require closed value types; unresolved authoring expressions are not executable",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -458,9 +474,29 @@ const collectIdentityIndexes = (
|
||||
for (const [interfaceIndex, revision] of workspace.interfaceImports.entries()) {
|
||||
const path = `interfaceImports[${interfaceIndex}]`;
|
||||
requireText(issues, revision.interfaceId, `${path}.interfaceId`, "Interface ID");
|
||||
if (revision.template)
|
||||
issue(issues, "invalid-value-type", path, "Unapplied generic interface cannot enter an installed workspace");
|
||||
requireText(issues, revision.revisionId, `${path}.revisionId`, "Interface revision ID");
|
||||
requireText(issues, revision.displayName, `${path}.displayName`, "Interface name");
|
||||
validateSource(issues, revision.source, `${path}.source`);
|
||||
if (revision.application) {
|
||||
try {
|
||||
if (
|
||||
appliedInterfaceId(revision.application) !== revision.revisionId ||
|
||||
revision.application.source.repository !== revision.source.repository ||
|
||||
revision.application.source.commit !== revision.source.commit
|
||||
)
|
||||
issue(
|
||||
issues,
|
||||
"invalid-value-type",
|
||||
path,
|
||||
"Applied interface identity does not match its exact provenance and arguments",
|
||||
);
|
||||
} catch (error) {
|
||||
if (!(error instanceof GenericTypeError)) throw error;
|
||||
issue(issues, "invalid-value-type", path, error.message);
|
||||
}
|
||||
}
|
||||
const memberIds = new Set<string>();
|
||||
const operations = new Map<OperationId, InterfaceOperationEntry>();
|
||||
for (const [memberIndex, member] of revision.members.entries()) {
|
||||
@@ -516,6 +552,35 @@ const collectIdentityIndexes = (
|
||||
const exports = new Map<string, PackageExport>();
|
||||
for (const [exportIndex, entry] of revision.exports.entries()) {
|
||||
const exportPath = `${path}.exports[${exportIndex}]`;
|
||||
if (entry.application) {
|
||||
try {
|
||||
const definition = revision.genericExports?.find((candidate) => candidate.id === entry.application!.exportId);
|
||||
if (!definition) throw new Error("Missing generic export definition");
|
||||
const expected = specializePackageExport(revision, definition, entry.application.arguments, {
|
||||
arguments: new Map(),
|
||||
self: entry.application.self,
|
||||
applyInterface: (id, args) => {
|
||||
const applied = workspace.interfaceImports.find(
|
||||
(contract) =>
|
||||
contract.application?.definitionId === id && isDeepStrictEqual(contract.application.arguments, args),
|
||||
);
|
||||
if (applied) return applied.revisionId;
|
||||
const concrete = workspace.interfaceImports.find((contract) => contract.revisionId === id);
|
||||
if (concrete && !concrete.template && args.length === 0) return id;
|
||||
throw new Error(`Missing closed interface application ${id}`);
|
||||
},
|
||||
// Retained obligations are discharged against candidate conformances below.
|
||||
implementsInterface: (target, required) =>
|
||||
(revision.argumentRequirements ?? []).some(
|
||||
(obligation) => obligation.required === required && isDeepStrictEqual(obligation.target, target),
|
||||
),
|
||||
});
|
||||
if (!isDeepStrictEqual(entry, expected))
|
||||
throw new Error("Specialized export differs from its definition, source or arguments");
|
||||
} catch (error) {
|
||||
issue(issues, "invalid-operation", exportPath, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
requireText(issues, entry.id, `${exportPath}.id`, "Package export ID");
|
||||
requireText(issues, entry.displayName, `${exportPath}.displayName`, "Package export name");
|
||||
if (exports.has(entry.id)) {
|
||||
@@ -655,8 +720,54 @@ const validateInterfaces = (
|
||||
issues: CapabilityValidationIssue[],
|
||||
indexes: ValidationIndexes,
|
||||
) => {
|
||||
const implies = (
|
||||
actual: InterfaceRevisionId,
|
||||
required: InterfaceRevisionId,
|
||||
visited = new Set<string>(),
|
||||
): boolean => {
|
||||
if (actual === required) return true;
|
||||
if (visited.has(actual)) return false;
|
||||
visited.add(actual);
|
||||
return (indexes.interfaces.get(actual)?.revision.requiredInterfaces ?? []).some((entry) =>
|
||||
implies(entry, required, visited),
|
||||
);
|
||||
};
|
||||
for (const [packageIndex, pkg] of workspace.packageImports.entries()) {
|
||||
for (const obligation of pkg.argumentRequirements ?? []) {
|
||||
const satisfied =
|
||||
obligation.target.kind === "atom"
|
||||
? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required))
|
||||
: implies(obligation.target.interfaceRevisionId, obligation.required);
|
||||
if (!satisfied)
|
||||
issue(
|
||||
issues,
|
||||
"unsatisfied-interface",
|
||||
`packageImports[${packageIndex}]`,
|
||||
`Generic argument does not implement ${obligation.required}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const [interfaceIndex, revision] of workspace.interfaceImports.entries()) {
|
||||
const path = `interfaceImports[${interfaceIndex}]`;
|
||||
for (const required of revision.requiredInterfaces ?? []) {
|
||||
if (!indexes.interfaces.has(required))
|
||||
issue(issues, "unresolved-reference", path, `Unknown prerequisite interface ${required}`);
|
||||
else if (implies(required, revision.revisionId))
|
||||
issue(
|
||||
issues,
|
||||
"cyclic-conformance-requirement",
|
||||
path,
|
||||
`Cyclic prerequisite involving ${revision.revisionId} and ${required}`,
|
||||
);
|
||||
}
|
||||
for (const obligation of revision.argumentRequirements ?? []) {
|
||||
const satisfied =
|
||||
obligation.target.kind === "atom"
|
||||
? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required))
|
||||
: implies(obligation.target.interfaceRevisionId, obligation.required);
|
||||
if (!satisfied)
|
||||
issue(issues, "unsatisfied-interface", path, `Generic argument does not implement ${obligation.required}`);
|
||||
}
|
||||
for (const [memberIndex, member] of revision.members.entries()) {
|
||||
const memberPath = `${path}.members[${memberIndex}]`;
|
||||
if (member.kind === "value") {
|
||||
@@ -1282,6 +1393,12 @@ const validateConformances = (
|
||||
}
|
||||
|
||||
const bindings = new Map<OperationId, Binding>();
|
||||
for (const required of interfaceEntry.revision.requiredInterfaces ?? []) {
|
||||
const requiredKey = conformanceKey(conformance.atomId, required);
|
||||
if (!indexes.conformances.has(requiredKey))
|
||||
issue(issues, "unsatisfied-interface", path, `Conformance requires ${conformance.atomId} as ${required}`);
|
||||
else requirementGraph.get(key)?.add(requiredKey);
|
||||
}
|
||||
for (const [bindingIndex, entry] of conformance.operationBindings.entries()) {
|
||||
const bindingPath = `${path}.operationBindings[${bindingIndex}]`;
|
||||
if (bindings.has(entry.operationId)) {
|
||||
@@ -1957,6 +2074,9 @@ export const computeCapabilityClosure = (
|
||||
atomId: conformance.source.atomId,
|
||||
interfaceRevisionId: conformance.source.interfaceRevisionId,
|
||||
});
|
||||
for (const interfaceRevisionId of plan.interfaces.get(root.interfaceRevisionId)?.requiredInterfaces ?? []) {
|
||||
queued.push({ atomId: root.atomId, interfaceRevisionId });
|
||||
}
|
||||
for (const binding of conformance.operationBindings.values()) {
|
||||
if (binding.kind === "state") {
|
||||
attachments.add(binding.slotId);
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { createRequire } from "node:module";
|
||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
||||
import { generateTypeScriptBindings, generatePackageDescriptor } from "../src/bindings/index.js";
|
||||
import { genericImplementationType } from "../src/bindings/generics.js";
|
||||
import { compileWorkspaceRevision, runtimeContracts } from "../src/capability-model/index.js";
|
||||
import { generateAppliedClientContracts } from "../src/bindings/client.js";
|
||||
const source = { repository: "https://example.test/generic.git", commit: "a".repeat(40) };
|
||||
test("generic port aliases retain their defining scope, including shadowed alias names", () => {
|
||||
const iface = compileCapabilityResourceSource(
|
||||
'type Box<value V> = list<V>; interface Data<value V> id "data" revision "data@1" {value payload id "payload" : Box<V> {get id "payload:get";}}',
|
||||
{ source },
|
||||
);
|
||||
assert.ok(iface.ok, JSON.stringify(iface.diagnostics));
|
||||
if (iface.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
'import interface Data; type Box<value V> = optional<V>; package P id "p" revision "p@1" {operation fetch<value V> id "data@1" : unit -> list<Box<V>> mode call receiver any requires {interface data id "data" : Data<Box<V>>;};}',
|
||||
{ source, environment: { interfaces: new Map([["Data", iface.resource.revision]]) } },
|
||||
);
|
||||
assert.ok(pkg.ok, JSON.stringify(pkg.diagnostics));
|
||||
if (pkg.resource.kind !== "package") throw new Error("expected package");
|
||||
const generated = generateTypeScriptBindings(
|
||||
{
|
||||
format: "quixos-bindings",
|
||||
version: 1,
|
||||
interfaces: [],
|
||||
interfaceTemplates: [iface.resource.revision],
|
||||
packages: [pkg.resource.revision],
|
||||
},
|
||||
pkg.resource.revision.revisionId,
|
||||
);
|
||||
assert.match(generated, /"payload.get":\(\)=>Promise<Array<\(T0 \| null\)>>/);
|
||||
});
|
||||
const definition = () => {
|
||||
const result = compileCapabilityResourceSource(
|
||||
`package Generic id "generic" revision "generic@1" {
|
||||
operation echo<value T> id "echo" : T -> T mode call receiver any;
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
if (result.resource.kind !== "package") throw new Error("expected package");
|
||||
return result.resource.revision;
|
||||
};
|
||||
test("generic package exports specialize per binding without changing immutable source identity", () => {
|
||||
const iface = compileCapabilityResourceSource(
|
||||
`interface Echo<value T> id "echo-interface" revision "echo-interface@1" {
|
||||
operation echo id "echo-member" : T -> T {call id "echo-call";}
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(iface.ok);
|
||||
if (iface.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const pkg = definition();
|
||||
const result = compileCapabilitySource(
|
||||
`workspace W id "w" revision "w@1" commit "${source.commit}" {
|
||||
import interface Echo; import package Generic; atom Thing id "thing";
|
||||
conform Thing as Echo<string> id "string-echo" {bind echo.call to package Generic.echo<string>;}
|
||||
conform Thing as Echo<list<int64>> id "list-echo" {bind echo.call to package Generic.echo<list<int64>>;}
|
||||
}`,
|
||||
"workspace.qx",
|
||||
{ interfaces: new Map([["Echo", iface.resource.revision]]), packages: new Map([["Generic", pkg]]) },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
assert.equal(pkg.exports.length, 0, "source definition was not mutated");
|
||||
const closed = result.workspace.packageImports[0];
|
||||
assert.equal(closed.revisionId, pkg.revisionId);
|
||||
assert.equal(closed.exports.length, 2);
|
||||
assert.notEqual(closed.exports[0].id, closed.exports[1].id);
|
||||
const oneApplication = structuredClone(result.workspace);
|
||||
oneApplication.packageImports[0].exports.splice(1);
|
||||
oneApplication.conformances.splice(1);
|
||||
assert.notDeepEqual(
|
||||
runtimeContracts(oneApplication),
|
||||
runtimeContracts(result.workspace),
|
||||
"New specializations invalidate the executable contract even with unchanged package source",
|
||||
);
|
||||
const schema = {
|
||||
format: "quixos-bindings" as const,
|
||||
version: 1 as const,
|
||||
interfaces: result.workspace.interfaceImports,
|
||||
packages: [closed],
|
||||
};
|
||||
const generated = generateTypeScriptBindings(schema, pkg.revisionId);
|
||||
assert.match(generated, /"echo":\s*\(<T0>/);
|
||||
for (const entry of closed.exports) {
|
||||
assert.ok(generated.includes(entry.id));
|
||||
assert.ok(generatePackageDescriptor(schema, pkg.revisionId).includes(entry.id));
|
||||
}
|
||||
const forged = structuredClone(result.workspace);
|
||||
forged.packageImports[0].exports[0].outputType = { kind: "scalar", name: "bool" };
|
||||
const rejected = compileWorkspaceRevision(forged);
|
||||
assert.equal(rejected.ok, false);
|
||||
if (!rejected.ok) assert.ok(rejected.issues.some((issue) => issue.message.includes("Specialized export differs")));
|
||||
});
|
||||
test("generated universal implementations compile and cannot assume a concrete value type", async () => {
|
||||
const pkg = definition();
|
||||
const signature = genericImplementationType(pkg.genericExports![0], [], () => {
|
||||
throw new Error("unexpected concrete type");
|
||||
});
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "qx-generic-ts-"));
|
||||
const require = createRequire(import.meta.url);
|
||||
const compiler = path.join(path.dirname(require.resolve("typescript/package.json")), "bin/tsc");
|
||||
const run = promisify(execFile);
|
||||
const preamble = `type QxObjectRef<T extends string>={readonly identity:T}; type QxContextLifecycle<C>={signal?:AbortSignal}; type Handler=${signature};\n`;
|
||||
try {
|
||||
const file = path.join(directory, "generic.ts");
|
||||
await fs.writeFile(file, preamble + `const handler:Handler=({input})=>input;`);
|
||||
await run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]);
|
||||
await fs.writeFile(file, preamble + `const handler:Handler=({input})=>"not universally T";`);
|
||||
await assert.rejects(
|
||||
run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]),
|
||||
(error) => {
|
||||
assert.match(String((error as { stdout: string }).stdout), /not assignable/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("generic Self follows the receiver parameter, never a preceding export", () => {
|
||||
const compile = (declaration: string) =>
|
||||
compileCapabilityResourceSource(`package P id "p" revision "p@1" {${declaration}}`, { source });
|
||||
const valid = compile('operation identity<object T> id "identity" : unit -> ref<Self> mode call receiver object T;');
|
||||
assert.ok(valid.ok, JSON.stringify(valid.diagnostics));
|
||||
if (valid.resource.kind !== "package") throw new Error("expected package");
|
||||
const definition = valid.resource.revision.genericExports![0];
|
||||
assert.deepEqual(definition.outputType, {
|
||||
kind: "object-ref",
|
||||
expectation: { kind: "parameter", parameterId: definition.parameters[0].id },
|
||||
});
|
||||
assert.equal(compile('function identity<value T> id "identity" : T -> ref<Self>;').ok, false);
|
||||
assert.equal(
|
||||
compile('operation events<value T> id "events" : unit -> watch-handle mode watch-start receiver any;').ok,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("typed presentation and factory consumers preserve distinct closed applications and return targets", async () => {
|
||||
const interfaces = new Map();
|
||||
for (const text of [
|
||||
'interface Presentation<value Props> id "presentation" revision "presentation@1" {operation props id "props" : unit -> Props {call id "props:get";}}',
|
||||
'interface Factory<object Result> id "factory" revision "factory@1" {operation create id "create" : unit -> ref<Result> {call id "create:call";}}',
|
||||
]) {
|
||||
const result = compileCapabilityResourceSource(text, { source });
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
interfaces.set(result.resource.revision.displayName, result.resource.revision);
|
||||
}
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
`import interface Presentation; import interface Factory;
|
||||
external atom Note id "note"; external atom Notebook id "notebook";
|
||||
package Views id "views" revision "views@1" {
|
||||
operation note id "note-view" : unit -> unit mode call receiver any requires {interface props id "props" : Presentation<record {title:string; note:atom-ref<Note>;}>;};
|
||||
operation notebook id "notebook-view" : unit -> unit mode call receiver any requires {interface props id "props" : Presentation<record {count:int32;}>;};
|
||||
operation factory id "factory-view" : unit -> unit mode call receiver any requires {interface factory id "factory" : Factory<atom Note>;};
|
||||
}`,
|
||||
{ source, environment: { interfaces } },
|
||||
);
|
||||
assert.ok(pkg.ok, JSON.stringify(pkg.diagnostics));
|
||||
const closed = pkg.resource.specializations!;
|
||||
const presentation = closed.find(
|
||||
(contract) =>
|
||||
contract.application?.definitionId === "presentation@1" && JSON.stringify(contract).includes('"title"'),
|
||||
)!;
|
||||
const factory = closed.find((contract) => contract.application?.definitionId === "factory@1")!;
|
||||
const contracts = generateAppliedClientContracts(closed);
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "qx-presentation-types-"));
|
||||
const compiler = path.join(
|
||||
path.dirname(createRequire(import.meta.url).resolve("typescript/package.json")),
|
||||
"bin/tsc",
|
||||
);
|
||||
const run = promisify(execFile);
|
||||
const usage = `type Props=CapabilityOutput<${JSON.stringify(presentation.revisionId)},"props:get">;
|
||||
type Created=CapabilityOutput<${JSON.stringify(factory.revisionId)},"create:call">;
|
||||
const render=({camino,render}:{camino:Props;render:{onSelect:(note:Created)=>void;compact:boolean}})=>{render.onSelect(camino.note);return camino.title;};\n`;
|
||||
try {
|
||||
const file = path.join(directory, "consumer.ts");
|
||||
await fs.writeFile(file, contracts + usage);
|
||||
await run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]);
|
||||
await fs.writeFile(file, contracts + usage + "const wrong=(props:Props)=>props.count;");
|
||||
await assert.rejects(
|
||||
run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]),
|
||||
(error) => {
|
||||
assert.match(String((error as { stdout: string }).stdout), /count.*does not exist/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,601 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
||||
import { generateTypeScriptBindings } from "../src/bindings/index.js";
|
||||
import { generateClientContracts } from "../src/bindings/client.js";
|
||||
import {
|
||||
TypeSubstitution,
|
||||
appliedInterfaceId,
|
||||
bindTypeParameters,
|
||||
canonicalTypeArgument,
|
||||
instantiateInterface,
|
||||
capabilityId,
|
||||
isStorableType,
|
||||
valueType,
|
||||
computeCapabilityClosure,
|
||||
compileWorkspaceRevision,
|
||||
type ClosedTypeArgument,
|
||||
type GenericTypeEnvironment,
|
||||
type ValueTypeExpression,
|
||||
} from "../src/capability-model/index.js";
|
||||
|
||||
test("unused generic definitions prove symbolic bounds and storable aliases", () => {
|
||||
const source = { repository: "https://example.test/bounds.git", commit: "a".repeat(40) };
|
||||
const interfaces = new Map();
|
||||
for (const text of [
|
||||
'interface Named id "named" revision "named@1" {}',
|
||||
'import interface Named; interface Detailed id "detailed" revision "detailed@1" requires Named {}',
|
||||
'import interface Named; interface Requires<object T implements Named> id "requires" revision "requires@1" {}',
|
||||
'interface Stored<value V : storable> id "stored" revision "stored@1" {}',
|
||||
]) {
|
||||
const result = compileCapabilityResourceSource(text, { source, environment: { interfaces } });
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
assert.equal(result.resource.kind, "interface");
|
||||
interfaces.set(result.resource.revision.displayName, result.resource.revision);
|
||||
}
|
||||
const compile = (body: string) => compileCapabilityResourceSource(body, { source, environment: { interfaces } });
|
||||
const bad = compile(
|
||||
'import interface Requires; interface Bad<object T> id "bad" revision "bad@1" requires Requires<T> {}',
|
||||
);
|
||||
assert.equal(bad.ok, false);
|
||||
assert.match(JSON.stringify(bad.diagnostics), /does not prove/);
|
||||
const good = compile(
|
||||
'import interface Detailed; import interface Requires; interface Good<object T implements Detailed> id "good" revision "good@1" requires Requires<T> {}',
|
||||
);
|
||||
assert.ok(good.ok, JSON.stringify(good.diagnostics));
|
||||
const alias = compile(
|
||||
'import interface Stored; type Values<value V : storable> = list<optional<V>>; interface Good<value T : storable> id "good" revision "good@1" requires Stored<Values<T>> {}',
|
||||
);
|
||||
assert.ok(alias.ok, JSON.stringify(alias.diagnostics));
|
||||
const nonstorable = compile(
|
||||
'import interface Stored; interface Bad<value T> id "bad" revision "bad@1" requires Stored<T> {}',
|
||||
);
|
||||
assert.equal(nonstorable.ok, false);
|
||||
});
|
||||
|
||||
const note = capabilityId.atom("atom:note");
|
||||
test("Self remains contextual through local aliases", () => {
|
||||
const result = compileCapabilityResourceSource(
|
||||
'type Me = ref<Self>; type Mine = optional<Me>; interface Identity id "identity" revision "identity@1" {value mine id "mine" : Mine {get id "mine:get";}}',
|
||||
{ source: { repository: "https://example.test/self.git", commit: "a".repeat(40) } },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
if (result.resource.kind !== "interface") throw new Error("expected interface");
|
||||
assert.equal(result.resource.revision.template?.usesSelf, true);
|
||||
const closed = instantiateInterface(result.resource.revision, [], { ...environment(), self: note });
|
||||
assert.deepEqual(closed.members[0].operations[0].outputType, valueType.optional(valueType.atomRef(note)));
|
||||
});
|
||||
const named = capabilityId.interfaceRevision("interface:named@1");
|
||||
const environment = (arguments_: [string, ClosedTypeArgument][] = []): GenericTypeEnvironment => ({
|
||||
arguments: new Map(arguments_),
|
||||
applyInterface: (id, args) => {
|
||||
assert.equal(args.length, 0);
|
||||
return id;
|
||||
},
|
||||
implementsInterface: (target, required) => target.kind === "atom" && target.atomId === note && required === named,
|
||||
});
|
||||
|
||||
test("substitution reaches nested records, lists, optionals and object references", () => {
|
||||
const substitute = new TypeSubstitution(
|
||||
environment([
|
||||
["scope/value", { kind: "value", type: valueType.int64 }],
|
||||
["scope/object", { kind: "object", target: { kind: "atom", atomId: note } }],
|
||||
]),
|
||||
);
|
||||
const result = substitute.value({
|
||||
kind: "record",
|
||||
fields: {
|
||||
values: { kind: "list", value: { kind: "optional", value: { kind: "parameter", parameterId: "scope/value" } } },
|
||||
target: { kind: "object-ref", expectation: { kind: "parameter", parameterId: "scope/object" } },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
kind: "record",
|
||||
fields: {
|
||||
values: valueType.list(valueType.optional(valueType.int64)),
|
||||
target: valueType.atomRef(note),
|
||||
},
|
||||
});
|
||||
assert.equal(isStorableType(result), false);
|
||||
});
|
||||
|
||||
test("Self is the exact implementing atom and cannot be unbound", () => {
|
||||
const expression = { kind: "object-ref", expectation: { kind: "self" } } as const;
|
||||
assert.throws(() => new TypeSubstitution(environment()).value(expression), /Self requires an implementing atom/);
|
||||
assert.deepEqual(new TypeSubstitution({ ...environment(), self: note }).value(expression), valueType.atomRef(note));
|
||||
});
|
||||
|
||||
test("parameter kinds and missing parameters fail closed", () => {
|
||||
const substitute = new TypeSubstitution(
|
||||
environment([["T", { kind: "object", target: { kind: "atom", atomId: note } }]]),
|
||||
);
|
||||
assert.throws(() => substitute.value({ kind: "parameter", parameterId: "T" }), /use ref<T>/);
|
||||
assert.throws(() => substitute.value({ kind: "parameter", parameterId: "unknown" }), /Unbound parameter/);
|
||||
assert.throws(
|
||||
() => bindTypeParameters([{ id: "T", name: "T", kind: "value" }], [], environment()),
|
||||
/Expected 1 type arguments/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
bindTypeParameters(
|
||||
[{ id: "T", name: "T", kind: "value" }],
|
||||
[{ kind: "object", target: { kind: "atom", atomId: note } }],
|
||||
environment(),
|
||||
),
|
||||
/Expected value, received object/,
|
||||
);
|
||||
});
|
||||
|
||||
test("bounds require evidence and storable constraints inspect nested values", () => {
|
||||
const parameters = [
|
||||
{ id: "T", name: "T", kind: "object", implements: [{ definitionId: named, arguments: [] }] },
|
||||
] as const;
|
||||
const mutableParameters = parameters.map((p) => ({
|
||||
...p,
|
||||
implements: p.implements.map((b) => ({ ...b, arguments: [] })),
|
||||
}));
|
||||
assert.equal(
|
||||
bindTypeParameters(mutableParameters, [{ kind: "object", target: { kind: "atom", atomId: note } }], environment())
|
||||
.size,
|
||||
1,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
bindTypeParameters(
|
||||
mutableParameters,
|
||||
[{ kind: "object", target: { kind: "atom", atomId: capabilityId.atom("person") } }],
|
||||
environment(),
|
||||
),
|
||||
/does not implement/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
bindTypeParameters(
|
||||
[{ kind: "value", id: "V", name: "V", storable: true }],
|
||||
[{ kind: "value", type: valueType.list(valueType.optional(valueType.atomRef(note))) }],
|
||||
environment(),
|
||||
),
|
||||
/cannot contain managed references/,
|
||||
);
|
||||
assert.equal(isStorableType(valueType.message("opaque")), false);
|
||||
assert.equal(isStorableType(valueType.watchHandle), false);
|
||||
assert.equal(
|
||||
isStorableType(valueType.message("checked"), (id) => id === "checked"),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("closed applications canonicalize records without erasing nominal identity or provenance", () => {
|
||||
const a: ClosedTypeArgument = {
|
||||
kind: "value",
|
||||
type: { kind: "record", fields: { b: valueType.int64, a: valueType.string } },
|
||||
};
|
||||
const b: ClosedTypeArgument = {
|
||||
kind: "value",
|
||||
type: { kind: "record", fields: { a: valueType.string, b: valueType.int64 } },
|
||||
};
|
||||
assert.equal(canonicalTypeArgument(a), canonicalTypeArgument(b));
|
||||
const application = {
|
||||
definitionId: named,
|
||||
source: { repository: "https://example.test/named.git", commit: "a".repeat(40) },
|
||||
arguments: [a],
|
||||
};
|
||||
assert.equal(appliedInterfaceId(application), appliedInterfaceId({ ...application, arguments: [b] }));
|
||||
assert.notEqual(appliedInterfaceId(application), appliedInterfaceId({ ...application, self: note }));
|
||||
assert.notEqual(
|
||||
appliedInterfaceId(application),
|
||||
appliedInterfaceId({ ...application, source: { ...application.source, commit: "b".repeat(40) } }),
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
canonicalTypeArgument({
|
||||
kind: "value",
|
||||
type: { kind: "parameter", parameterId: "T" },
|
||||
} as unknown as ClosedTypeArgument),
|
||||
/Expected a closed value type/,
|
||||
);
|
||||
});
|
||||
|
||||
test("aliases use lexical parameter identities, preserve outer bindings and reject cycles", () => {
|
||||
const env = environment([["outer/T", { kind: "value", type: valueType.string }]]);
|
||||
env.aliases = new Map([
|
||||
[
|
||||
"Box",
|
||||
{
|
||||
id: "Box",
|
||||
parameters: [{ id: "box/T", name: "T", kind: "value" }],
|
||||
body: { kind: "list", value: { kind: "parameter", parameterId: "box/T" } },
|
||||
},
|
||||
],
|
||||
["Loop", { id: "Loop", parameters: [], body: { kind: "alias", definitionId: "Loop", arguments: [] } }],
|
||||
]);
|
||||
const substitute = new TypeSubstitution(env);
|
||||
assert.deepEqual(
|
||||
substitute.value({
|
||||
kind: "alias",
|
||||
definitionId: "Box",
|
||||
arguments: [{ kind: "value", type: { kind: "parameter", parameterId: "outer/T" } }],
|
||||
}),
|
||||
valueType.list(valueType.string),
|
||||
);
|
||||
assert.deepEqual(substitute.value({ kind: "parameter", parameterId: "outer/T" }), valueType.string);
|
||||
assert.throws(() => substitute.value({ kind: "alias", definitionId: "Loop", arguments: [] }), /Loop -> Loop/);
|
||||
});
|
||||
|
||||
test("excessively deep types produce a bounded diagnostic", () => {
|
||||
let expression: ValueTypeExpression = valueType.string;
|
||||
for (let index = 0; index < 200; index++) expression = { kind: "list", value: expression };
|
||||
assert.throws(() => new TypeSubstitution(environment()).value(expression), /depth or expansion budget/);
|
||||
});
|
||||
|
||||
const source = { repository: "https://example.test/contracts.git", commit: "a".repeat(40) };
|
||||
const reader = () => {
|
||||
const result = compileCapabilityResourceSource(
|
||||
`interface Reader<value V> id "reader" revision "reader@1" {
|
||||
value values id "values" : list<optional<V>> { get id "values:get"; watch start id "values:watch" stop id "values:stop"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
assert.equal(result.resource.kind, "interface");
|
||||
if (result.resource.kind !== "interface") throw new Error("expected interface");
|
||||
return result.resource.revision;
|
||||
};
|
||||
|
||||
test("generic interface source retains its template and produces closed package/codegen contracts", () => {
|
||||
const definition = reader();
|
||||
assert.equal(definition.template?.parameters[0].name, "V");
|
||||
const result = compileCapabilityResourceSource(
|
||||
`import interface Reader;
|
||||
package Client id "client" revision "client@1" {
|
||||
function run id "run" : unit -> list<optional<string>> requires { interface reader id "reader-port" : Reader<string>; };
|
||||
}`,
|
||||
{ source, environment: { interfaces: new Map([["Reader", definition]]) } },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
if (result.resource.kind !== "package") throw new Error("expected package");
|
||||
assert.equal(result.resource.specializations?.length, 1);
|
||||
const closed = result.resource.specializations![0];
|
||||
assert.deepEqual(closed.members[0].operations[0].outputType, valueType.list(valueType.optional(valueType.string)));
|
||||
assert.deepEqual(closed.members[0].operations[1].eventType, closed.members[0].operations[0].outputType);
|
||||
const generated = generateTypeScriptBindings(
|
||||
{ format: "quixos-bindings", version: 1, interfaces: [closed], packages: [result.resource.revision] },
|
||||
"client@1",
|
||||
);
|
||||
assert.match(generated, /Array<\(string \| null\)>/);
|
||||
assert.ok(generated.includes(closed.revisionId));
|
||||
});
|
||||
|
||||
test("generic conformances bind state against specialized signatures", () => {
|
||||
const result = compileCapabilitySource(
|
||||
`workspace Demo id "ws" revision "ws@1" commit "${source.commit}" {
|
||||
atom Document id "document";
|
||||
import interface Reader;
|
||||
conform Document as Reader<string> id "reader-conformance" {
|
||||
private state Values id "values-slot" on Document : list<optional<string>> policy optimistic-register;
|
||||
bind values.get to state Values.read;
|
||||
bind values.watch-start to state Values.watch-start;
|
||||
bind values.watch-stop to state Values.watch-stop;
|
||||
}
|
||||
}`,
|
||||
"workspace.qx",
|
||||
{ interfaces: new Map([["Reader", reader()]]) },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
assert.equal(result.workspace.interfaceImports.length, 1);
|
||||
assert.equal(result.workspace.interfaceImports[0].template, undefined);
|
||||
assert.equal(result.workspace.conformances[0].interfaceRevisionId, result.workspace.interfaceImports[0].revisionId);
|
||||
});
|
||||
|
||||
test("source rejects wrong generic arity and an unapplied interface", () => {
|
||||
for (const input of [
|
||||
"interface-ref<Reader>",
|
||||
"interface-ref<Reader<string, int32>>",
|
||||
"interface-ref<Reader<atom Note>>",
|
||||
]) {
|
||||
const result = compileCapabilityResourceSource(
|
||||
`import interface Reader; external atom Note id "note";
|
||||
package P id "p" revision "p@1" { function f id "f" : ${input} -> unit; }`,
|
||||
{ source, environment: { interfaces: new Map([["Reader", reader()]]) } },
|
||||
);
|
||||
assert.equal(result.ok, false, input);
|
||||
assert.match(result.diagnostics[0].code, /type-arity|parameter-kind/);
|
||||
}
|
||||
});
|
||||
|
||||
test("package receivers and injected dependencies select exact applications", () => {
|
||||
const definition = reader();
|
||||
const summary = compileCapabilityResourceSource(
|
||||
`interface Summary id "summary" revision "summary@1" {
|
||||
value summary id "summary-value" : list<optional<string>> { get id "summary:get"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(summary.ok);
|
||||
if (summary.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
`import interface Reader;
|
||||
package P id "p" revision "p@1" {
|
||||
operation summarize id "summarize" : unit -> list<optional<string>> mode call receiver interfaces [Reader<string>]
|
||||
requires { interface reader id "reader-port" : Reader<string>; };
|
||||
}`,
|
||||
{ source, environment: { interfaces: new Map([["Reader", definition]]) } },
|
||||
);
|
||||
assert.ok(pkg.ok, JSON.stringify(pkg.diagnostics));
|
||||
if (pkg.resource.kind !== "package") throw new Error("expected package");
|
||||
const result = compileCapabilitySource(
|
||||
`workspace W id "w" revision "w@1" commit "${source.commit}" {
|
||||
import interface Reader; import interface Summary; import package P;
|
||||
atom Note id "note";
|
||||
conform Note as Reader<string> id "reader-conformance" {
|
||||
private state Values id "values" on Note : list<optional<string>> policy optimistic-register;
|
||||
bind values.get to state Values.read;
|
||||
bind values.watch-start to state Values.watch-start;
|
||||
bind values.watch-stop to state Values.watch-stop;
|
||||
}
|
||||
conform Note as Summary id "summary-conformance" {
|
||||
bind summary.get to package P.summarize with { reader to interface Reader<string>; };
|
||||
}
|
||||
}`,
|
||||
"workspace.qx",
|
||||
{
|
||||
interfaces: new Map([
|
||||
["Reader", definition],
|
||||
["Summary", summary.resource.revision],
|
||||
]),
|
||||
interfaceClosure: pkg.resource.specializations,
|
||||
packages: new Map([["P", pkg.resource.revision]]),
|
||||
},
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
});
|
||||
|
||||
test("named generic value aliases elaborate through nested lists", () => {
|
||||
const result = compileCapabilityResourceSource(
|
||||
`type Page<value T> = record { items: list<T>; next: optional<string>; };
|
||||
package P id "p" revision "p@1" { function f id "f" : unit -> Page<int64>; }`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
if (result.resource.kind !== "package") throw new Error("expected package");
|
||||
assert.deepEqual(result.resource.revision.exports[0].outputType, {
|
||||
kind: "record",
|
||||
fields: { items: valueType.list(valueType.int64), next: valueType.optional(valueType.string) },
|
||||
});
|
||||
});
|
||||
|
||||
test("Self specializes to the implementing atom rather than an erased interface", () => {
|
||||
const definition = compileCapabilityResourceSource(
|
||||
`interface Identity id "identity" revision "identity@1" {
|
||||
operation identity id "identity-member" : ref<Self> -> ref<Self> { call id "identity-call"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(definition.ok, JSON.stringify(definition.diagnostics));
|
||||
if (definition.resource.kind !== "interface") throw new Error("expected interface");
|
||||
assert.equal(definition.resource.revision.template?.usesSelf, true);
|
||||
const { revision } = definition.resource;
|
||||
const first = instantiateInterface(revision, [], { ...environment(), self: note });
|
||||
const second = instantiateInterface(revision, [], { ...environment(), self: capabilityId.atom("other") });
|
||||
assert.deepEqual(first.members[0].operations[0].outputType, valueType.atomRef(note));
|
||||
assert.notEqual(first.revisionId, second.revisionId);
|
||||
});
|
||||
|
||||
test("package Self comes from an exact receiver, never a previous export", () => {
|
||||
const valid = compileCapabilityResourceSource(
|
||||
`type Owned<object T> = ref<T>; external atom Note id "note";
|
||||
package P id "p" revision "p@1" {
|
||||
operation identity id "identity" : ref<Self> -> Owned<Self> mode call receiver atom Note;
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(valid.ok, JSON.stringify(valid.diagnostics));
|
||||
if (valid.resource.kind !== "package") throw new Error("expected package");
|
||||
assert.deepEqual(valid.resource.revision.exports[0].outputType, valueType.atomRef(capabilityId.atom("note")));
|
||||
const invalid = compileCapabilityResourceSource(
|
||||
`external atom Note id "note";
|
||||
package P id "p" revision "p@1" {
|
||||
operation identity id "identity" : ref<Self> -> ref<Self> mode call receiver atom Note;
|
||||
function bad id "bad" : unit -> ref<Self>;
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.equal(invalid.ok, false);
|
||||
assert.equal(invalid.diagnostics[0].code, "unbound-self");
|
||||
});
|
||||
|
||||
test("host generation rejects unresolved definitions and operation-only dispatch ambiguity", () => {
|
||||
const definition = reader();
|
||||
assert.throws(() => generateClientContracts([definition], {}), /closed interface/);
|
||||
const first = instantiateInterface(definition, [{ kind: "value", type: valueType.string }], environment());
|
||||
const second = instantiateInterface(definition, [{ kind: "value", type: valueType.int32 }], environment());
|
||||
assert.throws(() => generateClientContracts([first, second], {}), /ambiguous/);
|
||||
});
|
||||
|
||||
test("finite recursive generic interface references share the same closed application", () => {
|
||||
const definition = compileCapabilityResourceSource(
|
||||
`interface Node<value V> id "node" revision "node@1" {
|
||||
value next id "next" : optional<interface-ref<Node<V>>> { get id "next:get"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(definition.ok, JSON.stringify(definition.diagnostics));
|
||||
if (definition.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const result = compileCapabilityResourceSource(
|
||||
`import interface Node;
|
||||
package P id "p" revision "p@1" { function f id "f" : interface-ref<Node<string>> -> unit; }
|
||||
`,
|
||||
{ source, environment: { interfaces: new Map([["Node", definition.resource.revision]]) } },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
const [closed] = result.resource.specializations!;
|
||||
assert.equal(result.resource.specializations!.length, 1);
|
||||
assert.deepEqual(
|
||||
closed.members[0].operations[0].outputType,
|
||||
valueType.optional(valueType.interfaceRef(closed.revisionId)),
|
||||
);
|
||||
});
|
||||
|
||||
test("expanding recursive generic interfaces fail with a bounded diagnostic", () => {
|
||||
const definition = compileCapabilityResourceSource(
|
||||
`interface Node<value V> id "node" revision "node@1" {
|
||||
value next id "next" : interface-ref<Node<list<V>>> { get id "next:get"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(definition.ok, JSON.stringify(definition.diagnostics));
|
||||
if (definition.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const result = compileCapabilityResourceSource(
|
||||
`import interface Node;
|
||||
package P id "p" revision "p@1" { function f id "f" : interface-ref<Node<string>> -> unit; }
|
||||
`,
|
||||
{ source, environment: { interfaces: new Map([["Node", definition.resource.revision]]) } },
|
||||
);
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.diagnostics[0].code, "recursive-application");
|
||||
});
|
||||
|
||||
test("storable parameters do not imply support for RPC-only records", () => {
|
||||
assert.equal(isStorableType({ kind: "record", fields: { name: valueType.string } }), false);
|
||||
assert.equal(isStorableType(valueType.list(valueType.optional(valueType.string))), true);
|
||||
});
|
||||
|
||||
test("generic declarations reject duplicate members before specialization", () => {
|
||||
const result = compileCapabilityResourceSource(
|
||||
`interface Bad<value V> id "bad" revision "bad@1" {
|
||||
value first id "duplicate" : V { get id "first:get"; }
|
||||
value second id "duplicate" : V { get id "second:get"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.diagnostics[0].code, "duplicate-interface-member");
|
||||
});
|
||||
|
||||
test("unused templates still check nested application arity and alias cycles", () => {
|
||||
const malformed = compileCapabilityResourceSource(
|
||||
`import interface Reader;
|
||||
interface Bad<value V> id "bad" revision "bad@1" {
|
||||
value item id "item" : interface-ref<Reader<V, V>> { get id "get"; }
|
||||
}`,
|
||||
{ source, environment: { interfaces: new Map([["Reader", reader()]]) } },
|
||||
);
|
||||
assert.equal(malformed.ok, false);
|
||||
assert.equal(malformed.diagnostics[0].code, "type-arity");
|
||||
const cycle = compileCapabilityResourceSource(
|
||||
`type Loop<value V> = list<Loop<V>>;
|
||||
interface Bad<value V> id "bad" revision "bad@1" {
|
||||
value item id "item" : Loop<V> { get id "get"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.equal(cycle.ok, false);
|
||||
assert.equal(cycle.diagnostics[0].code, "recursive-alias");
|
||||
});
|
||||
|
||||
test("Self in a prerequisite contributes to the outer application identity", () => {
|
||||
const identity = compileCapabilityResourceSource(
|
||||
`interface Identity id "identity" revision "identity@1" {
|
||||
value self id "self" : ref<Self> { get id "self:get"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(identity.ok);
|
||||
if (identity.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const outer = compileCapabilityResourceSource(
|
||||
`import interface Identity;
|
||||
interface Outer<value V> id "outer" revision "outer@1" requires Identity {}`,
|
||||
{ source, environment: { interfaces: new Map([["Identity", identity.resource.revision]]) } },
|
||||
);
|
||||
assert.ok(outer.ok, JSON.stringify(outer.diagnostics));
|
||||
if (outer.resource.kind !== "interface") throw new Error("expected interface");
|
||||
assert.equal(outer.resource.revision.template?.usesSelf, true);
|
||||
});
|
||||
|
||||
test("object bounds are discharged against the complete candidate, not source order", () => {
|
||||
const namedResult = compileCapabilityResourceSource(`interface Named id "named" revision "named@1" {}`, { source });
|
||||
assert.ok(namedResult.ok);
|
||||
if (namedResult.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const named = namedResult.resource.revision;
|
||||
const bounded = compileCapabilityResourceSource(
|
||||
`import interface Named;
|
||||
interface Container<object T implements Named> id "container" revision "container@1" {}`,
|
||||
{ source, environment: { interfaces: new Map([["Named", named]]) } },
|
||||
);
|
||||
assert.ok(bounded.ok, JSON.stringify(bounded.diagnostics));
|
||||
if (bounded.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const container = bounded.resource.revision;
|
||||
const compile = (evidence: string) =>
|
||||
compileCapabilitySource(
|
||||
`workspace W id "w" revision "w@1" commit "${source.commit}" {
|
||||
import interface Named; import interface Container;
|
||||
atom Note id "note"; atom Index id "index";
|
||||
conform Index as Container<atom Note> id "index-container" {}
|
||||
${evidence}
|
||||
}`,
|
||||
"workspace.qx",
|
||||
{
|
||||
interfaces: new Map([
|
||||
["Named", named],
|
||||
["Container", container],
|
||||
]),
|
||||
},
|
||||
);
|
||||
const missing = compile("");
|
||||
assert.equal(missing.ok, false);
|
||||
assert.ok(missing.diagnostics.some((issue) => issue.code === "unsatisfied-interface"));
|
||||
const valid = compile('conform Note as Named id "note-named" {}');
|
||||
assert.ok(valid.ok, JSON.stringify(valid.diagnostics));
|
||||
});
|
||||
|
||||
test("prerequisites require explicit conformances and remain in the capability closure", () => {
|
||||
const baseResult = compileCapabilityResourceSource(`interface Base id "base" revision "base@1" {}`, { source });
|
||||
assert.ok(baseResult.ok);
|
||||
if (baseResult.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const base = baseResult.resource.revision;
|
||||
const derivedResult = compileCapabilityResourceSource(
|
||||
`import interface Base;
|
||||
interface Derived<value V> id "derived" revision "derived@1" requires Base {}`,
|
||||
{
|
||||
source,
|
||||
environment: { interfaces: new Map([["Base", base]]) },
|
||||
},
|
||||
);
|
||||
assert.ok(derivedResult.ok, JSON.stringify(derivedResult.diagnostics));
|
||||
if (derivedResult.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const derived = derivedResult.resource.revision;
|
||||
const compile = (evidence: string) =>
|
||||
compileCapabilitySource(
|
||||
`workspace W id "w" revision "w@1" commit "${source.commit}" {
|
||||
import interface Base; import interface Derived; atom Note id "note";
|
||||
conform Note as Derived<string> id "derived-conformance" {}
|
||||
${evidence}
|
||||
}`,
|
||||
"workspace.qx",
|
||||
{
|
||||
interfaces: new Map([
|
||||
["Base", base],
|
||||
["Derived", derived],
|
||||
]),
|
||||
},
|
||||
);
|
||||
assert.equal(compile("").ok, false);
|
||||
const result = compile('conform Note as Base id "base-conformance" {}');
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
const root = result.workspace.conformances[0];
|
||||
const closure = computeCapabilityClosure(result.plan, [root]);
|
||||
assert.equal(closure.conformances.length, 2);
|
||||
const unresolved = structuredClone(result.workspace);
|
||||
unresolved.interfaceImports[0].members.push({
|
||||
kind: "value",
|
||||
id: capabilityId.member("unresolved"),
|
||||
displayName: "unresolved",
|
||||
operations: [],
|
||||
valueType: { kind: "parameter", parameterId: "T" } as unknown as typeof valueType.string,
|
||||
});
|
||||
assert.equal(compileWorkspaceRevision(unresolved).ok, false);
|
||||
const forged = structuredClone(result.workspace);
|
||||
const applied = forged.interfaceImports.find((entry) => entry.application)!;
|
||||
applied.application!.arguments = [{ kind: "value", type: valueType.int64 }];
|
||||
assert.equal(compileWorkspaceRevision(forged).ok, false);
|
||||
});
|
||||
Reference in New Issue
Block a user