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:
@@ -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}` : ""}`;
|
||||
};
|
||||
Reference in New Issue
Block a user