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