Files
quixos-protocol/src/capability-language/parser.ts
T

1584 lines
42 KiB
TypeScript
Raw Normal View History

import {
BaseErrorListener,
CharStream,
CommonTokenStream,
type ATNSimulator,
type ParserRuleContext,
type RecognitionException,
type Recognizer,
type Token,
} from "antlr4ng";
import {
capabilityId,
cardinalityValueType,
compileWorkspaceRevision,
valueType,
type AtomId,
type BoundDependency,
type CompiledWorkspaceRevision,
type DependencyPort,
type EdgeCardinality,
type EdgeDefinition,
type EdgeEndpoint,
type EdgeEndpointConstraint,
type EdgePrimitive,
type EdgeTraversal,
type InterfaceMember,
type InterfaceOperation,
type InterfaceOperationMode,
type InterfaceRevision,
type PackageExport,
type PackageReceiverRequirement,
type PackageRevision,
type PersistentAttachment,
type SourceRevision,
type StatePrimitive,
type StateSlotDefinition,
type ValueType,
type WorkspaceRevision,
} from "../capability-model/index.js";
import { QuixosCapabilityLexer } from "./generated/QuixosCapabilityLexer.js";
import { QuixosCapabilityParser,
type AttachmentDeclContext,
type ConformanceDeclContext,
type DependencyBindingBlockContext,
type DependencyPortContext,
type EdgeDeclContext,
type EdgeEndpointContext,
type InterfaceDeclContext,
type PackageConstructorExportContext,
type PackageDeclContext,
type PackageFunctionExportContext,
type PackageOperationExportContext,
type OperationMemberContext,
type SourceBlockContext,
type StateDeclContext,
type TargetConstraintContext,
type ValueMemberContext,
type ValueTypeContext,
type RelationshipMemberContext,
type RelationshipMaterializationDeclContext,
type WorkspaceDeclContext,
} from "./generated/QuixosCapabilityParser.js";
export type CapabilitySourceDiagnosticPhase =
| "syntax"
| "lowering"
| "validation";
export interface CapabilitySourceDiagnostic {
phase: CapabilitySourceDiagnosticPhase;
code: string;
message: string;
fileName: string;
line: number;
column: number;
path?: string;
}
export type CapabilitySourceCompileResult =
| {
ok: true;
workspace: WorkspaceRevision;
plan: CompiledWorkspaceRevision;
diagnostics: [];
}
| {
ok: false;
diagnostics: CapabilitySourceDiagnostic[];
};
interface InterfaceSymbol {
revisionId: InterfaceRevision["revisionId"];
members: Map<
string,
{
memberId: InterfaceMember["id"];
operations: Map<string, InterfaceOperation["id"]>;
}
>;
}
interface PackageExportSymbol {
exportId: PackageExport["id"];
ports: Map<string, DependencyPort["id"]>;
}
interface PackageSymbol {
revisionId: PackageRevision["revisionId"];
exports: Map<string, PackageExportSymbol>;
}
interface AttachmentSymbol {
attachment: PersistentAttachment;
projections: Map<string, EdgeEndpoint["projectionId"]>;
}
interface LoweringState {
fileName: string;
diagnostics: CapabilitySourceDiagnostic[];
atoms: Map<string, AtomId>;
interfaces: Map<string, InterfaceSymbol>;
packages: Map<string, PackageSymbol>;
attachments: Map<string, AttachmentSymbol>;
}
class SyntaxErrorListener extends BaseErrorListener {
constructor(
private readonly fileName: string,
private readonly diagnostics: CapabilitySourceDiagnostic[],
) {
super();
}
override syntaxError<S extends Token, T extends ATNSimulator>(
_recognizer: Recognizer<T>,
_offendingSymbol: S | null,
line: number,
column: number,
message: string,
_error: RecognitionException | null,
) {
this.diagnostics.push({
phase: "syntax",
code: "syntax-error",
message,
fileName: this.fileName,
line,
column,
});
}
}
const contextPosition = (context: ParserRuleContext) => ({
line: context.start?.line ?? 0,
column: context.start?.column ?? 0,
});
const loweringIssue = (
state: LoweringState,
context: ParserRuleContext,
code: string,
message: string,
) => {
state.diagnostics.push({
phase: "lowering",
code,
message,
fileName: state.fileName,
...contextPosition(context),
});
};
const text = (context: { getText(): string } | null): string => {
if (!context) {
throw new Error("ANTLR returned a missing context after a successful parse");
}
return context.getText();
};
const identifier = text;
const stringValue = (context: { getText(): string } | null) =>
JSON.parse(text(context)) as string;
const declareSymbol = <Value>(
state: LoweringState,
table: Map<string, Value>,
name: string,
value: Value,
context: ParserRuleContext,
kind: string,
) => {
if (table.has(name)) {
loweringIssue(
state,
context,
"duplicate-symbol",
`Duplicate ${kind} authoring name ${name}`,
);
return;
}
table.set(name, value);
};
const requireSymbol = <Value>(
state: LoweringState,
table: ReadonlyMap<string, Value>,
name: string,
context: ParserRuleContext,
kind: string,
): Value | undefined => {
const value = table.get(name);
if (value === undefined) {
loweringIssue(
state,
context,
"unknown-symbol",
`Unknown ${kind} ${name}`,
);
}
return value;
};
const lowerSource = (context: SourceBlockContext): SourceRevision => ({
repository: stringValue(context.stringLiteral(0)),
commit: stringValue(context.stringLiteral(1)),
});
const lowerCardinality = (
context: { getText(): string } | null,
): EdgeCardinality => text(context) as EdgeCardinality;
const lowerConstraint = (
state: LoweringState,
context: TargetConstraintContext | null,
): EdgeEndpointConstraint | undefined => {
if (!context) {
throw new Error("Missing target constraint after a successful parse");
}
const name = identifier(context.identifier());
if (context.ATOM()) {
const atomId = requireSymbol(state, state.atoms, name, context, "atom");
return atomId ? { kind: "atom", atomId } : undefined;
}
const symbol = requireSymbol(
state,
state.interfaces,
name,
context,
"interface",
);
return symbol
? { kind: "interface", interfaceRevisionId: symbol.revisionId }
: undefined;
};
const lowerValueType = (
state: LoweringState,
context: ValueTypeContext | null,
): ValueType => {
if (!context) {
throw new Error("Missing value type after a successful parse");
}
const scalar = context.scalarType();
if (scalar) {
return { kind: "scalar", name: text(scalar) as never };
}
if (context.UNIT()) {
return valueType.unit;
}
if (context.WATCH_HANDLE()) {
return valueType.watchHandle;
}
if (context.MESSAGE()) {
return valueType.message(stringValue(context.stringLiteral()));
}
if (context.ATOM_REF()) {
const name = identifier(context.identifier());
const atomId = requireSymbol(state, state.atoms, name, context, "atom");
return valueType.atomRef(atomId ?? capabilityId.atom(`unresolved:${name}`));
}
if (context.INTERFACE_REF()) {
const name = identifier(context.identifier());
const symbol = requireSymbol(
state,
state.interfaces,
name,
context,
"interface",
);
return valueType.interfaceRef(
symbol?.revisionId ?? capabilityId.interfaceRevision(`unresolved:${name}`),
);
}
if (context.OPTIONAL()) {
return valueType.optional(lowerValueType(state, context.valueType()));
}
if (context.LIST()) {
return valueType.list(lowerValueType(state, context.valueType()));
}
loweringIssue(state, context, "invalid-type", "Unrecognized value type");
return valueType.unit;
};
const valueOperation = (
displayName: string,
id: InterfaceOperation["id"],
value: ValueType,
): InterfaceOperation => {
switch (displayName) {
case "get":
return {
id,
displayName,
inputType: valueType.unit,
outputType: value,
mode: "call",
};
case "set":
return {
id,
displayName,
inputType: value,
outputType: valueType.unit,
mode: "call",
};
case "watch-start":
return {
id,
displayName,
inputType: valueType.unit,
outputType: valueType.watchHandle,
eventType: value,
mode: "watch-start",
};
case "watch-stop":
return {
id,
displayName,
inputType: valueType.watchHandle,
outputType: valueType.unit,
mode: "watch-stop",
};
default:
throw new Error(`Unknown value operation ${displayName}`);
}
};
const relationshipOperation = (
displayName: string,
id: InterfaceOperation["id"],
target: EdgeEndpointConstraint,
cardinality: EdgeCardinality,
): InterfaceOperation => {
const targetType =
target.kind === "atom"
? valueType.atomRef(target.atomId)
: valueType.interfaceRef(target.interfaceRevisionId);
const resolvedType = cardinalityValueType(target, cardinality);
switch (displayName) {
case "resolve":
return {
id,
displayName,
inputType: valueType.unit,
outputType: resolvedType,
mode: "call",
};
case "connect":
case "disconnect":
return {
id,
displayName,
inputType: targetType,
outputType: valueType.unit,
mode: "call",
};
case "watch-start":
return {
id,
displayName,
inputType: valueType.unit,
outputType: valueType.watchHandle,
eventType: resolvedType,
mode: "watch-start",
};
case "watch-stop":
return {
id,
displayName,
inputType: valueType.watchHandle,
outputType: valueType.unit,
mode: "watch-stop",
};
default:
throw new Error(`Unknown relationship operation ${displayName}`);
}
};
const lowerValueMember = (
state: LoweringState,
context: ValueMemberContext,
): InterfaceMember => {
const memberName = identifier(context.identifier());
const memberValueType = lowerValueType(state, context.valueType());
const operations: InterfaceOperation[] = [];
for (const operation of context.valueMemberOperation()) {
if (operation.GET()) {
operations.push(
valueOperation(
"get",
capabilityId.operation(stringValue(operation.stringLiteral(0))),
memberValueType,
),
);
} else if (operation.SET()) {
operations.push(
valueOperation(
"set",
capabilityId.operation(stringValue(operation.stringLiteral(0))),
memberValueType,
),
);
} else {
operations.push(
valueOperation(
"watch-start",
capabilityId.operation(stringValue(operation.stringLiteral(0))),
memberValueType,
),
valueOperation(
"watch-stop",
capabilityId.operation(stringValue(operation.stringLiteral(1))),
memberValueType,
),
);
}
}
return {
kind: "value",
id: capabilityId.member(stringValue(context.stringLiteral())),
displayName: memberName,
valueType: memberValueType,
operations,
};
};
const lowerRelationshipMember = (
state: LoweringState,
context: RelationshipMemberContext,
): InterfaceMember | undefined => {
const target = lowerConstraint(state, context.targetConstraint());
if (!target) {
return undefined;
}
const cardinality = lowerCardinality(context.cardinality());
const operations: InterfaceOperation[] = [];
for (const operation of context.relationshipOperation()) {
if (operation.RESOLVE()) {
operations.push(
relationshipOperation(
"resolve",
capabilityId.operation(stringValue(operation.stringLiteral(0))),
target,
cardinality,
),
);
} else if (operation.CONNECT()) {
operations.push(
relationshipOperation(
"connect",
capabilityId.operation(stringValue(operation.stringLiteral(0))),
target,
cardinality,
),
);
} else if (operation.DISCONNECT()) {
operations.push(
relationshipOperation(
"disconnect",
capabilityId.operation(stringValue(operation.stringLiteral(0))),
target,
cardinality,
),
);
} else {
operations.push(
relationshipOperation(
"watch-start",
capabilityId.operation(stringValue(operation.stringLiteral(0))),
target,
cardinality,
),
relationshipOperation(
"watch-stop",
capabilityId.operation(stringValue(operation.stringLiteral(1))),
target,
cardinality,
),
);
}
}
return {
kind: "relationship",
id: capabilityId.member(stringValue(context.stringLiteral())),
displayName: identifier(context.identifier()),
target,
cardinality,
ordered: Boolean(context.ORDERED()),
operations,
};
};
const lowerOperationMember = (
state: LoweringState,
context: OperationMemberContext,
): InterfaceMember => {
const inputType = lowerValueType(state, context.valueType(0));
const outputType = lowerValueType(state, context.valueType(1));
return {
kind: "operation",
id: capabilityId.member(stringValue(context.stringLiteral(0))),
displayName: identifier(context.identifier()),
inputType,
outputType,
operations: [{
id: capabilityId.operation(stringValue(context.stringLiteral(1))),
displayName: "call",
inputType,
outputType,
mode: "call",
}],
};
};
const lowerInterface = (
state: LoweringState,
context: InterfaceDeclContext,
): InterfaceRevision => {
const alias = identifier(context.identifier());
const members = context.interfaceMember().flatMap((entry) => {
const value = entry.valueMember();
if (value) {
return [lowerValueMember(state, value)];
}
const operation = entry.operationMember();
if (operation) {
return [lowerOperationMember(state, operation)];
}
const relationship = lowerRelationshipMember(
state,
entry.relationshipMember()!,
);
return relationship ? [relationship] : [];
});
const symbol = state.interfaces.get(alias)!;
for (const member of members) {
declareSymbol(
state,
symbol.members,
member.displayName,
{
memberId: member.id,
operations: new Map(
member.operations.map((operation) => [operation.displayName, operation.id]),
),
},
context,
"interface member",
);
}
return {
interfaceId: capabilityId.interface(stringValue(context.stringLiteral(0))),
revisionId: symbol.revisionId,
displayName: alias,
source: lowerSource(context.sourceBlock()),
members,
};
};
const lowerDependencyPort = (
state: LoweringState,
context: DependencyPortContext,
): DependencyPort | undefined => {
const name = identifier(context.identifier(0));
const id = capabilityId.dependencyPort(stringValue(context.stringLiteral()));
if (context.STATE()) {
const primitives = context
.primitiveList()!
.primitive()
.map((entry) => text(entry));
const invalid = primitives.filter(
(entry) => !["read", "write", "watch-start", "watch-stop"].includes(entry),
);
if (invalid.length > 0) {
loweringIssue(
state,
context,
"invalid-port-primitive",
`State port cannot request ${invalid.join(", ")}`,
);
}
return {
id,
displayName: name,
requirement: {
kind: "state",
valueType: lowerValueType(state, context.valueType()),
primitives: primitives.filter((entry) => !invalid.includes(entry)) as StatePrimitive[],
},
};
}
if (context.EDGE()) {
const target = lowerConstraint(state, context.targetConstraint());
if (!target) {
return undefined;
}
const primitives = context
.primitiveList()!
.primitive()
.map((entry) => text(entry));
const invalid = primitives.filter(
(entry) =>
!["resolve", "connect", "disconnect", "watch-start", "watch-stop"].includes(
entry,
),
);
if (invalid.length > 0) {
loweringIssue(
state,
context,
"invalid-port-primitive",
`Edge port cannot request ${invalid.join(", ")}`,
);
}
return {
id,
displayName: name,
requirement: {
kind: "edge",
target,
cardinality: lowerCardinality(context.cardinality()),
primitives: primitives.filter((entry) => !invalid.includes(entry)) as EdgePrimitive[],
},
};
}
const targetName = identifier(context.identifier(1));
if (context.INTERFACE()) {
const target = requireSymbol(
state,
state.interfaces,
targetName,
context,
"interface",
);
return target
? {
id,
displayName: name,
requirement: {
kind: "receiver-interface",
interfaceRevisionId: target.revisionId,
},
}
: undefined;
}
const atomId = requireSymbol(state, state.atoms, targetName, context, "atom");
return atomId
? {
id,
displayName: name,
requirement: { kind: "constructor", atomId },
}
: undefined;
};
const lowerDependencyPorts = (
state: LoweringState,
context: { dependencyBlock(): ReturnType<PackageOperationExportContext["dependencyBlock"]> },
) => {
const block = context.dependencyBlock();
if (!block) {
return [];
}
return block
.dependencyPort()
.flatMap((entry) => {
const port = lowerDependencyPort(state, entry);
return port ? [port] : [];
});
};
const lowerReceiver = (
state: LoweringState,
context: PackageOperationExportContext["receiverRequirement"] extends () => infer Result
? Result
: never,
): PackageReceiverRequirement => {
if (context.ANY()) {
return { kind: "any-object" };
}
if (context.ATOM()) {
const name = identifier(context.identifier());
return {
kind: "exact-atom",
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);
return (
requireSymbol(state, state.interfaces, name, entry, "interface")?.revisionId ??
capabilityId.interfaceRevision(`unresolved:${name}`)
);
}),
};
};
const registerPackageExport = (
state: LoweringState,
packageAlias: string,
exportAlias: string,
entry: PackageExport,
context: ParserRuleContext,
) => {
const packageSymbol = state.packages.get(packageAlias)!;
const portSymbols = new Map<string, DependencyPort["id"]>();
for (const port of entry.dependencyPorts) {
declareSymbol(
state,
portSymbols,
port.displayName,
port.id,
context,
"dependency port",
);
}
declareSymbol(
state,
packageSymbol.exports,
exportAlias,
{ exportId: entry.id, ports: portSymbols },
context,
"package export",
);
};
const lowerPackageOperation = (
state: LoweringState,
packageAlias: string,
context: PackageOperationExportContext,
): PackageExport => {
const alias = identifier(context.identifier());
const ports = lowerDependencyPorts(state, context);
const event = context.eventClause();
const entry: PackageExport = {
kind: "operation",
id: capabilityId.packageExport(stringValue(context.stringLiteral())),
displayName: alias,
inputType: lowerValueType(state, context.valueType(0)),
outputType: lowerValueType(state, context.valueType(1)),
mode: text(context.operationMode()) as InterfaceOperationMode,
eventType: event ? lowerValueType(state, event.valueType()) : undefined,
receiverRequirement: lowerReceiver(state, context.receiverRequirement()),
dependencyPorts: ports,
};
registerPackageExport(state, packageAlias, alias, entry, context);
return entry;
};
const lowerPackageFunction = (
state: LoweringState,
packageAlias: string,
context: PackageFunctionExportContext,
): PackageExport => {
const alias = identifier(context.identifier());
const entry: PackageExport = {
kind: "function",
id: capabilityId.packageExport(stringValue(context.stringLiteral())),
displayName: alias,
inputType: lowerValueType(state, context.valueType(0)),
outputType: lowerValueType(state, context.valueType(1)),
dependencyPorts: lowerDependencyPorts(state, context),
};
registerPackageExport(state, packageAlias, alias, entry, context);
return entry;
};
const lowerPackageConstructor = (
state: LoweringState,
packageAlias: string,
context: PackageConstructorExportContext,
): PackageExport => {
const alias = identifier(context.identifier(0));
const atomName = identifier(context.identifier(1));
const atomId =
requireSymbol(state, state.atoms, atomName, context, "atom") ??
capabilityId.atom(`unresolved:${atomName}`);
const entry: PackageExport = {
kind: "constructor",
id: capabilityId.packageExport(stringValue(context.stringLiteral())),
displayName: alias,
inputType: lowerValueType(state, context.valueType()),
outputType: valueType.atomRef(atomId),
constructsAtom: atomId,
dependencyPorts: lowerDependencyPorts(state, context),
};
registerPackageExport(state, packageAlias, alias, entry, context);
return entry;
};
const lowerPackage = (
state: LoweringState,
context: PackageDeclContext,
): PackageRevision => {
const alias = identifier(context.identifier());
const exports = context.packageExport().map((exportContext) => {
const operation = exportContext.packageOperationExport();
if (operation) {
return lowerPackageOperation(state, alias, operation);
}
const fn = exportContext.packageFunctionExport();
if (fn) {
return lowerPackageFunction(state, alias, fn);
}
return lowerPackageConstructor(
state,
alias,
exportContext.packageConstructorExport()!,
);
});
return {
packageId: capabilityId.package(stringValue(context.stringLiteral(0))),
revisionId: state.packages.get(alias)!.revisionId,
displayName: alias,
source: lowerSource(context.sourceBlock()),
exports,
};
};
const lowerState = (
state: LoweringState,
context: StateDeclContext,
): StateSlotDefinition => {
const atomName = identifier(context.identifier(1));
const atomId =
requireSymbol(state, state.atoms, atomName, context, "atom") ??
capabilityId.atom(`unresolved:${atomName}`);
const policy = context.storagePolicy();
const defaultContext = context.jsonLiteral();
let defaultValue: unknown;
if (defaultContext) {
defaultValue = JSON.parse(text(defaultContext)) as unknown;
}
return {
kind: "state",
id: capabilityId.slot(stringValue(context.stringLiteral())),
attachedTo: atomId,
displayName: identifier(context.identifier(0)),
valueType: lowerValueType(state, context.valueType()),
storagePolicy: policy.OPTIMISTIC_REGISTER()
? { kind: "optimistic-register" }
: {
kind: "crdt-document",
updateType: lowerValueType(state, policy.valueType()),
},
defaultValue,
};
};
const lowerEdgeEndpoint = (
state: LoweringState,
context: EdgeEndpointContext,
): EdgeEndpoint | undefined => {
const constraint = lowerConstraint(state, context.targetConstraint());
return constraint
? {
projectionId: capabilityId.edgeProjection(
stringValue(context.stringLiteral()),
),
displayName: identifier(context.identifier()),
constraint,
cardinality: lowerCardinality(context.cardinality()),
ordered: Boolean(context.ORDERED()),
}
: undefined;
};
const lowerEdge = (
state: LoweringState,
context: EdgeDeclContext,
): EdgeDefinition | undefined => {
const endpoints = context
.edgeEndpoint()
.flatMap((entry) => {
const endpoint = lowerEdgeEndpoint(state, entry);
return endpoint ? [endpoint] : [];
});
if (endpoints.length !== 2) {
return undefined;
}
return {
kind: "edge",
id: capabilityId.edgeType(stringValue(context.stringLiteral())),
displayName: identifier(context.identifier()),
endpoints: [endpoints[0]!, endpoints[1]!],
};
};
const lowerAttachment = (
state: LoweringState,
context: AttachmentDeclContext,
): PersistentAttachment | undefined => {
const stateContext = context.stateDecl();
return stateContext
? lowerState(state, stateContext)
: lowerEdge(state, context.edgeDecl()!);
};
const registerAttachment = (
state: LoweringState,
context: AttachmentDeclContext,
attachment: PersistentAttachment,
) => {
const stateContext = context.stateDecl();
const name = identifier(
stateContext ? stateContext.identifier(0) : context.edgeDecl()!.identifier(),
);
const projections = new Map<string, EdgeEndpoint["projectionId"]>();
if (attachment.kind === "edge") {
for (const endpoint of attachment.endpoints) {
declareSymbol(
state,
projections,
endpoint.displayName,
endpoint.projectionId,
context,
"edge projection",
);
}
}
declareSymbol(
state,
state.attachments,
name,
{ attachment, projections },
context,
"attachment",
);
};
const findOperationId = (
state: LoweringState,
interfaceAlias: string,
memberName: string,
operationName: string,
context: ParserRuleContext,
) => {
const interfaceSymbol = requireSymbol(
state,
state.interfaces,
interfaceAlias,
context,
"interface",
);
const member = interfaceSymbol
? requireSymbol(
state,
interfaceSymbol.members,
memberName,
context,
`member on ${interfaceAlias}`,
)
: undefined;
return member
? requireSymbol(
state,
member.operations,
operationName,
context,
`operation on ${interfaceAlias}.${memberName}`,
)
: undefined;
};
const lowerBoundDependencies = (
state: LoweringState,
context: DependencyBindingBlockContext | undefined,
exportSymbol: PackageExportSymbol,
): BoundDependency[] => {
if (!context) {
return [];
}
return context.dependencyBinding().flatMap<BoundDependency>((entry) => {
const portName = identifier(entry.identifier(0));
const portId = requireSymbol(
state,
exportSymbol.ports,
portName,
entry,
"dependency port",
);
if (!portId) {
return [];
}
const traversal = (
edgeName: string,
projectionName: string,
): EdgeTraversal | undefined => {
const attachment = requireSymbol(
state,
state.attachments,
edgeName,
entry,
"attachment",
);
if (!attachment || attachment.attachment.kind !== "edge") {
if (attachment) {
loweringIssue(state, entry, "wrong-attachment-kind", `${edgeName} is state, not an edge`);
}
return undefined;
}
const projectionId = requireSymbol(
state,
attachment.projections,
projectionName,
entry,
`projection on ${edgeName}`,
);
return projectionId
? { edgeTypeId: attachment.attachment.id, projectionId }
: undefined;
};
if (entry.STATE()) {
const attachmentName = identifier(entry.identifier(1));
const attachment = requireSymbol(
state,
state.attachments,
attachmentName,
entry,
"attachment",
);
if (!attachment || attachment.attachment.kind !== "state") {
if (attachment) {
loweringIssue(
state,
entry,
"wrong-attachment-kind",
`${attachmentName} is an edge, not state`,
);
}
return [];
}
const via = entry.VIA()
? traversal(identifier(entry.identifier(2)), identifier(entry.identifier(3)))
: undefined;
if (entry.VIA() && !via) return [];
return [{
portId,
binding: { kind: "state", slotId: attachment.attachment.id, ...(via ? { via } : {}) },
}];
}
if (entry.EDGE(0)) {
const attachmentName = identifier(entry.identifier(1));
const projectionName = identifier(entry.identifier(2));
const attachment = requireSymbol(
state,
state.attachments,
attachmentName,
entry,
"attachment",
);
if (!attachment || attachment.attachment.kind !== "edge") {
if (attachment) {
loweringIssue(
state,
entry,
"wrong-attachment-kind",
`${attachmentName} is state, not an edge`,
);
}
return [];
}
const projectionId = requireSymbol(
state,
attachment.projections,
projectionName,
entry,
`projection on ${attachmentName}`,
);
const via = entry.VIA()
? traversal(identifier(entry.identifier(3)), identifier(entry.identifier(4)))
: undefined;
if (entry.VIA() && !via) return [];
return projectionId
? [
{
portId,
binding: {
kind: "edge",
edgeTypeId: attachment.attachment.id,
projectionId,
...(via ? { via } : {}),
},
},
]
: [];
}
if (entry.INTERFACE()) {
const interfaceName = identifier(entry.identifier(1));
const interfaceSymbol = requireSymbol(
state,
state.interfaces,
interfaceName,
entry,
"interface",
);
return interfaceSymbol
? [
{
portId,
binding: {
kind: "receiver-interface",
interfaceRevisionId: interfaceSymbol.revisionId,
},
},
]
: [];
}
const atomName = identifier(entry.identifier(1));
const atomId = requireSymbol(state, state.atoms, atomName, entry, "atom");
return atomId
? [{ portId, binding: { kind: "constructor", atomId } }]
: [];
});
};
const lowerRelationshipMaterialization = (
state: LoweringState,
interfaceName: string,
context: RelationshipMaterializationDeclContext,
): WorkspaceRevision["conformances"][number]["relationshipMaterializations"][number] | undefined => {
const memberName = identifier(context.identifier(0));
const member = requireSymbol(
state,
state.interfaces.get(interfaceName)?.members ?? new Map(),
memberName,
context,
`member on ${interfaceName}`,
);
const constructorAtom = requireSymbol(
state,
state.atoms,
identifier(context.identifier(1)),
context,
"constructor atom",
);
const edgeName = identifier(context.identifier(2));
const edge = requireSymbol(state, state.attachments, edgeName, context, "attachment");
if (edge && edge.attachment.kind !== "edge") {
loweringIssue(state, context, "wrong-attachment-kind", `${edgeName} is state, not an edge`);
}
const projectionId = edge?.attachment.kind === "edge"
? requireSymbol(
state,
edge.projections,
identifier(context.identifier(3)),
context,
`projection on ${edgeName}`,
)
: undefined;
return member && constructorAtom && edge?.attachment.kind === "edge" && projectionId
? {
memberId: member.memberId,
constructorAtomId: constructorAtom,
edgeTypeId: edge.attachment.id,
constructedProjectionId: projectionId,
}
: undefined;
};
const lowerConformance = (
state: LoweringState,
context: ConformanceDeclContext,
privateAttachments: PersistentAttachment[],
): WorkspaceRevision["conformances"][number] | undefined => {
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",
);
if (!atomId || !interfaceSymbol) {
return undefined;
}
const operationBindings = context.conformanceItem().flatMap<
WorkspaceRevision["conformances"][number]["operationBindings"][number]
>((item) => {
const bindingContext = item.operationBindingDecl();
if (!bindingContext) {
return [];
}
const memberName = identifier(
bindingContext.memberOperationRef().identifier(),
);
const operationName = text(
bindingContext.memberOperationRef()?.operationName() ?? null,
);
const operationId = findOperationId(
state,
interfaceName,
memberName,
operationName,
bindingContext,
);
if (!operationId) {
return [];
}
const provider = bindingContext.operationProvider();
if (provider.STATE()) {
const attachmentName = identifier(provider.identifier(0));
const attachment = requireSymbol(
state,
state.attachments,
attachmentName,
provider,
"attachment",
);
if (!attachment || attachment.attachment.kind !== "state") {
if (attachment) {
loweringIssue(
state,
provider,
"wrong-attachment-kind",
`${attachmentName} is not state`,
);
}
return [];
}
return [
{
operationId,
binding: {
kind: "state" as const,
slotId: attachment.attachment.id,
primitive: text(provider.statePrimitive()) as StatePrimitive,
},
},
];
}
if (provider.EDGE()) {
const edgeName = identifier(provider.identifier(0));
const projectionName = identifier(provider.identifier(1));
const attachment = requireSymbol(
state,
state.attachments,
edgeName,
provider,
"attachment",
);
if (!attachment || attachment.attachment.kind !== "edge") {
if (attachment) {
loweringIssue(
state,
provider,
"wrong-attachment-kind",
`${edgeName} is not an edge`,
);
}
return [];
}
const projectionId = requireSymbol(
state,
attachment.projections,
projectionName,
provider,
`projection on ${edgeName}`,
);
return projectionId
? [
{
operationId,
binding: {
kind: "edge" as const,
edgeTypeId: attachment.attachment.id,
projectionId,
primitive: text(provider.edgePrimitive()) as EdgePrimitive,
},
},
]
: [];
}
const packageName = identifier(provider.identifier(0));
const exportName = identifier(provider.identifier(1));
const packageSymbol = requireSymbol(
state,
state.packages,
packageName,
provider,
"package",
);
const exportSymbol = packageSymbol
? requireSymbol(
state,
packageSymbol.exports,
exportName,
provider,
`export on ${packageName}`,
)
: undefined;
return packageSymbol && exportSymbol
? [
{
operationId,
binding: {
kind: "package" as const,
packageRevisionId: packageSymbol.revisionId,
exportId: exportSymbol.exportId,
dependencies: lowerBoundDependencies(
state,
provider.dependencyBindingBlock() ?? undefined,
exportSymbol,
),
},
},
]
: [];
});
const relationshipMaterializations = context.conformanceItem().flatMap((item) => {
const materialization = item.relationshipMaterializationDecl();
if (!materialization) return [];
const lowered = lowerRelationshipMaterialization(state, interfaceName, materialization);
return lowered ? [lowered] : [];
});
return {
atomId,
interfaceRevisionId: interfaceSymbol.revisionId,
privateAttachments,
operationBindings,
relationshipMaterializations,
};
};
const lowerWorkspace = (
state: LoweringState,
context: WorkspaceDeclContext,
): WorkspaceRevision => {
const items = context.workspaceItem();
for (const item of items) {
const atom = item.atomDecl();
if (atom) {
declareSymbol(
state,
state.atoms,
identifier(atom.identifier()),
capabilityId.atom(stringValue(atom.stringLiteral(0))),
atom,
"atom",
);
continue;
}
const interfaceContext = item.interfaceDecl();
if (interfaceContext) {
declareSymbol(
state,
state.interfaces,
identifier(interfaceContext.identifier()),
{
revisionId: capabilityId.interfaceRevision(
stringValue(interfaceContext.stringLiteral(1)),
),
members: new Map(),
},
interfaceContext,
"interface",
);
continue;
}
const packageContext = item.packageDecl();
if (packageContext) {
declareSymbol(
state,
state.packages,
identifier(packageContext.identifier()),
{
revisionId: capabilityId.packageRevision(
stringValue(packageContext.stringLiteral(1)),
),
exports: new Map(),
},
packageContext,
"package",
);
}
}
const atoms = items.flatMap((item) => {
const atom = item.atomDecl();
if (!atom) {
return [];
}
const atomId = state.atoms.get(identifier(atom.identifier()));
return atomId
? [
{
id: atomId,
displayName: identifier(atom.identifier()),
documentation: atom.DOC()
? stringValue(atom.stringLiteral(1))
: undefined,
},
]
: [];
});
const interfaceImports = items.flatMap((item) => {
const interfaceContext = item.interfaceDecl();
return interfaceContext ? [lowerInterface(state, interfaceContext)] : [];
});
const packageImports = items.flatMap((item) => {
const packageContext = item.packageDecl();
return packageContext ? [lowerPackage(state, packageContext)] : [];
});
const sharedAttachments: PersistentAttachment[] = [];
const privateAttachments = new Map<
ConformanceDeclContext,
PersistentAttachment[]
>();
for (const item of items) {
const shared = item.sharedAttachmentDecl();
if (shared) {
const declaration = shared.attachmentDecl();
const attachment = lowerAttachment(state, declaration);
if (attachment) {
sharedAttachments.push(attachment);
registerAttachment(state, declaration, attachment);
}
}
const conformance = item.conformanceDecl();
if (conformance) {
const attachments = conformance.conformanceItem().flatMap((entry) => {
const declaration = entry.attachmentDecl();
if (!declaration) {
return [];
}
const attachment = lowerAttachment(state, declaration);
if (!attachment) {
return [];
}
registerAttachment(state, declaration, attachment);
return [attachment];
});
privateAttachments.set(conformance, attachments);
}
}
const conformances = items.flatMap((item) => {
const context = item.conformanceDecl();
if (!context) {
return [];
}
const conformance = lowerConformance(
state,
context,
privateAttachments.get(context) ?? [],
);
return conformance ? [conformance] : [];
});
const constructors = items.flatMap((item) => {
const constructor = item.constructorBindingDecl();
if (!constructor) {
return [];
}
const atomName = identifier(constructor.identifier(0));
const packageName = identifier(constructor.identifier(1));
const exportName = identifier(constructor.identifier(2));
const atomId = requireSymbol(state, state.atoms, atomName, constructor, "atom");
const packageSymbol = requireSymbol(
state,
state.packages,
packageName,
constructor,
"package",
);
const exportSymbol = packageSymbol
? requireSymbol(
state,
packageSymbol.exports,
exportName,
constructor,
`export on ${packageName}`,
)
: undefined;
return atomId && packageSymbol && exportSymbol
? [
{
atomId,
packageRevisionId: packageSymbol.revisionId,
exportId: exportSymbol.exportId,
dependencies: lowerBoundDependencies(
state,
constructor.dependencyBindingBlock() ?? undefined,
exportSymbol,
),
},
]
: [];
});
return {
id: capabilityId.workspaceRevision(stringValue(context.stringLiteral(1))),
workspaceId: capabilityId.workspace(stringValue(context.stringLiteral(0))),
parentRevisionIds: [],
sourceRootCommit: stringValue(context.stringLiteral(2)),
atoms,
sharedAttachments,
interfaceImports,
packageImports,
conformances,
constructors,
};
};
export const compileCapabilitySource = (
source: string,
fileName = "<memory>",
): CapabilitySourceCompileResult => {
const diagnostics: CapabilitySourceDiagnostic[] = [];
const listener = new SyntaxErrorListener(fileName, diagnostics);
const lexer = new QuixosCapabilityLexer(CharStream.fromString(source));
lexer.removeErrorListeners();
lexer.addErrorListener(listener);
const parser = new QuixosCapabilityParser(
new CommonTokenStream(lexer),
);
parser.removeErrorListeners();
parser.addErrorListener(listener);
const tree = parser.document();
if (diagnostics.length > 0) {
return { ok: false, diagnostics };
}
const state: LoweringState = {
fileName,
diagnostics,
atoms: new Map(),
interfaces: new Map(),
packages: new Map(),
attachments: new Map(),
};
const workspace = lowerWorkspace(state, tree.workspaceDecl());
if (diagnostics.length > 0) {
return { ok: false, diagnostics };
}
const compiled = compileWorkspaceRevision(workspace);
if (!compiled.ok) {
return {
ok: false,
diagnostics: compiled.issues.map((entry) => ({
phase: "validation",
code: entry.code,
message: entry.message,
fileName,
line: 0,
column: 0,
path: entry.path,
})),
};
}
return { ok: true, workspace, plan: compiled.plan, diagnostics: [] };
};