1985 lines
54 KiB
TypeScript
1985 lines
54 KiB
TypeScript
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 AtomDefinition,
|
|
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 DocumentContext,
|
|
type EdgeDeclContext,
|
|
type EdgeEndpointContext,
|
|
type ExternalAtomDeclContext,
|
|
type ExternalInterfaceDeclContext,
|
|
type InterfaceResourceDeclContext,
|
|
type PackageConstructorExportContext,
|
|
type PackageResourceDeclContext,
|
|
type PackageFunctionExportContext,
|
|
type PackageOperationExportContext,
|
|
type OperationMemberContext,
|
|
type ResourceImportDeclContext,
|
|
type ResourcePreambleContext,
|
|
type StateDeclContext,
|
|
type TargetConstraintContext,
|
|
type ValueMemberContext,
|
|
type ValueTypeContext,
|
|
type RelationshipMemberContext,
|
|
type RelationshipMaterializationDeclContext,
|
|
type WorkspaceDeclContext,
|
|
} from "./generated/QuixosCapabilityParser.js";
|
|
|
|
export type CapabilityResourceKind = "interface" | "package";
|
|
|
|
export type CapabilityResourceImport = {
|
|
kind: CapabilityResourceKind;
|
|
binding: string;
|
|
};
|
|
|
|
export type CapabilityExternalInterface = {
|
|
binding: string;
|
|
revisionId: InterfaceRevision["revisionId"];
|
|
};
|
|
|
|
export type CapabilityImportEnvironment = {
|
|
interfaces?: ReadonlyMap<string, InterfaceRevision>;
|
|
packages?: ReadonlyMap<string, PackageRevision>;
|
|
/** Complete resolved closure; direct maps above provide local authoring names. */
|
|
interfaceClosure?: readonly InterfaceRevision[];
|
|
packageClosure?: readonly PackageRevision[];
|
|
/** External nominal atoms required by imported resources. */
|
|
externalAtoms?: readonly AtomDefinition[];
|
|
/** External nominal interfaces supplied by the importing workspace. */
|
|
externalInterfaces?: readonly CapabilityExternalInterface[];
|
|
};
|
|
|
|
export type CapabilityResource =
|
|
| {
|
|
kind: "interface";
|
|
imports: CapabilityResourceImport[];
|
|
externalAtoms: AtomDefinition[];
|
|
externalInterfaces: CapabilityExternalInterface[];
|
|
revision: InterfaceRevision;
|
|
}
|
|
| {
|
|
kind: "package";
|
|
imports: CapabilityResourceImport[];
|
|
externalAtoms: AtomDefinition[];
|
|
externalInterfaces: CapabilityExternalInterface[];
|
|
revision: PackageRevision;
|
|
};
|
|
|
|
export type CapabilityResourceCompileResult =
|
|
| { ok: true; resource: CapabilityResource; diagnostics: [] }
|
|
| { ok: false; diagnostics: CapabilitySourceDiagnostic[] };
|
|
|
|
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;
|
|
imports: CapabilityResourceImport[];
|
|
diagnostics: [];
|
|
}
|
|
| {
|
|
ok: false;
|
|
diagnostics: CapabilitySourceDiagnostic[];
|
|
};
|
|
|
|
interface InterfaceSymbol {
|
|
revisionId: InterfaceRevision["revisionId"];
|
|
contractAvailable: boolean;
|
|
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 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.RECORD()) {
|
|
const fields = context.recordField().map((field) => [identifier(field.identifier()), lowerValueType(state, field.valueType())] as const);
|
|
if (new Set(fields.map(([name]) => name)).size !== fields.length) loweringIssue(state, context, "invalid-type", "Duplicate record field");
|
|
return {kind: "record", fields: Object.fromEntries(fields)};
|
|
}
|
|
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: InterfaceResourceDeclContext,
|
|
source: SourceRevision,
|
|
): 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,
|
|
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",
|
|
);
|
|
if (target && !target.contractAvailable) {
|
|
loweringIssue(
|
|
state,
|
|
context,
|
|
"nominal-interface-port",
|
|
`Interface port ${name} needs the full ${targetName} contract; use import interface ${targetName}`,
|
|
);
|
|
return undefined;
|
|
}
|
|
return target
|
|
? {
|
|
id,
|
|
displayName: name,
|
|
requirement: {
|
|
kind: "interface",
|
|
interfaceRevisionId: target.revisionId,
|
|
},
|
|
}
|
|
: undefined;
|
|
}
|
|
const atomId = requireSymbol(state, state.atoms, targetName, context, "atom");
|
|
return atomId
|
|
? {
|
|
id,
|
|
displayName: name,
|
|
requirement: { kind: "constructor", atomId,
|
|
...(context.valueType() ? { inputType: lowerValueType(state, context.valueType()) } : {}) },
|
|
}
|
|
: 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: PackageResourceDeclContext,
|
|
source: SourceRevision,
|
|
): 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,
|
|
...(context.INTEGER() ? { semanticMajor: Number(context.INTEGER()!.getText()) } : {}),
|
|
displayName: alias,
|
|
source,
|
|
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(0)),
|
|
),
|
|
displayName: identifier(context.identifier()),
|
|
constraint,
|
|
cardinality: lowerCardinality(context.cardinality()),
|
|
ordered: Boolean(context.ORDERED()),
|
|
...(context.ON_DELETE() ? { onDelete: stringValue(context.stringLiteral(1)) as EdgeEndpoint["onDelete"] } : {}),
|
|
...(context.RETAIN_OTHER() ? { retainOther: true } : {}),
|
|
...(context.KEYED() ? {keyType: stringValue(context.stringLiteral(context.ON_DELETE() ? 2 : 1)) as EdgeEndpoint["keyType"]} : {}),
|
|
...(context.PUBLIC_TRAVERSAL() ? {publicTraversal: true} : {}),
|
|
}
|
|
: 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) && !entry.INTERFACE()) {
|
|
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",
|
|
);
|
|
const via = entry.VIA()
|
|
? traversal(identifier(entry.identifier(2)), identifier(entry.identifier(3)))
|
|
: undefined;
|
|
if (entry.VIA() && !via) return [];
|
|
return interfaceSymbol
|
|
? [
|
|
{
|
|
portId,
|
|
binding: {
|
|
kind: "interface",
|
|
interfaceRevisionId: interfaceSymbol.revisionId,
|
|
...(via ? { via } : {}),
|
|
},
|
|
},
|
|
]
|
|
: [];
|
|
}
|
|
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,
|
|
...(context.stringLiteral() ? { id: capabilityId.conformance(stringValue(context.stringLiteral()!)) } : {}),
|
|
...(context.INTEGER() ? { semanticMajor: Number(context.INTEGER()!.getText()) } : {}),
|
|
privateAttachments,
|
|
operationBindings,
|
|
relationshipMaterializations,
|
|
};
|
|
};
|
|
|
|
const interfaceSymbolFor = (revision: InterfaceRevision): InterfaceSymbol => ({
|
|
revisionId: revision.revisionId,
|
|
contractAvailable: true,
|
|
members: new Map(revision.members.map((member) => [
|
|
member.displayName,
|
|
{
|
|
memberId: member.id,
|
|
operations: new Map(member.operations.map((operation) => [
|
|
operation.displayName,
|
|
operation.id,
|
|
])),
|
|
},
|
|
])),
|
|
});
|
|
|
|
const packageSymbolFor = (revision: PackageRevision): PackageSymbol => ({
|
|
revisionId: revision.revisionId,
|
|
exports: new Map(revision.exports.map((entry) => [
|
|
entry.displayName,
|
|
{
|
|
exportId: entry.id,
|
|
ports: new Map(entry.dependencyPorts.map((port) => [
|
|
port.displayName,
|
|
port.id,
|
|
])),
|
|
},
|
|
])),
|
|
});
|
|
|
|
const resourceImport = (
|
|
context: ResourceImportDeclContext,
|
|
): CapabilityResourceImport => ({
|
|
kind: context.INTERFACE() ? "interface" : "package",
|
|
binding: identifier(context.identifier()),
|
|
});
|
|
|
|
const registerImports = (
|
|
state: LoweringState,
|
|
contexts: readonly ResourceImportDeclContext[],
|
|
environment: CapabilityImportEnvironment,
|
|
) => contexts.map((context) => {
|
|
const imported = resourceImport(context);
|
|
if (imported.kind === "interface") {
|
|
const revision = environment.interfaces?.get(imported.binding);
|
|
if (!revision) {
|
|
loweringIssue(
|
|
state,
|
|
context,
|
|
"missing-import",
|
|
`No resolved interface is available for lock binding ${imported.binding}`,
|
|
);
|
|
} else {
|
|
declareSymbol(
|
|
state,
|
|
state.interfaces,
|
|
imported.binding,
|
|
interfaceSymbolFor(revision),
|
|
context,
|
|
"interface",
|
|
);
|
|
}
|
|
} else {
|
|
const revision = environment.packages?.get(imported.binding);
|
|
if (!revision) {
|
|
loweringIssue(
|
|
state,
|
|
context,
|
|
"missing-import",
|
|
`No resolved package is available for lock binding ${imported.binding}`,
|
|
);
|
|
} else {
|
|
declareSymbol(
|
|
state,
|
|
state.packages,
|
|
imported.binding,
|
|
packageSymbolFor(revision),
|
|
context,
|
|
"package",
|
|
);
|
|
}
|
|
}
|
|
return imported;
|
|
});
|
|
|
|
const uniqueExactRevisions = <Revision extends {
|
|
revisionId: string;
|
|
source: SourceRevision;
|
|
}>(revisions: readonly Revision[]): Revision[] => {
|
|
const seen = new Set<string>();
|
|
return revisions.filter((revision) => {
|
|
const key = `${revision.revisionId}\0${revision.source.repository}\0${revision.source.commit}`;
|
|
if (seen.has(key)) return false;
|
|
seen.add(key);
|
|
return true;
|
|
});
|
|
};
|
|
|
|
const uniqueAtoms = (atoms: readonly AtomDefinition[]): AtomDefinition[] => {
|
|
const seen = new Set<string>();
|
|
return atoms.filter((atom) => {
|
|
if (seen.has(atom.id)) return false;
|
|
seen.add(atom.id);
|
|
return true;
|
|
});
|
|
};
|
|
|
|
const lowerWorkspace = (
|
|
state: LoweringState,
|
|
context: WorkspaceDeclContext,
|
|
environment: CapabilityImportEnvironment,
|
|
): { workspace: WorkspaceRevision; imports: CapabilityResourceImport[] } => {
|
|
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 imports = registerImports(
|
|
state,
|
|
items.flatMap((item) => item.resourceImportDecl() ?? []),
|
|
environment,
|
|
);
|
|
|
|
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 = uniqueExactRevisions([
|
|
...(environment.interfaceClosure ?? []),
|
|
...[...(environment.interfaces?.values() ?? [])],
|
|
]);
|
|
|
|
const packageImports = uniqueExactRevisions([
|
|
...(environment.packageClosure ?? []),
|
|
...[...(environment.packages?.values() ?? [])],
|
|
]);
|
|
|
|
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 {
|
|
imports,
|
|
workspace: {
|
|
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 parseDocument = (
|
|
source: string,
|
|
fileName: string,
|
|
): { tree: DocumentContext; tokens: CommonTokenStream; diagnostics: CapabilitySourceDiagnostic[] } => {
|
|
const diagnostics: CapabilitySourceDiagnostic[] = [];
|
|
const listener = new SyntaxErrorListener(fileName, diagnostics);
|
|
const lexer = new QuixosCapabilityLexer(CharStream.fromString(source));
|
|
lexer.removeErrorListeners();
|
|
lexer.addErrorListener(listener);
|
|
const tokens = new CommonTokenStream(lexer);
|
|
const parser = new QuixosCapabilityParser(tokens);
|
|
parser.removeErrorListeners();
|
|
parser.addErrorListener(listener);
|
|
const tree = parser.document();
|
|
tokens.fill();
|
|
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 validationDiagnostics = (
|
|
fileName: string,
|
|
issues: ReturnType<typeof compileWorkspaceRevision> extends infer _Result
|
|
? Array<{ code: string; message: string; path: string }>
|
|
: never,
|
|
): CapabilitySourceDiagnostic[] => issues.map((entry) => ({
|
|
phase: "validation",
|
|
code: entry.code,
|
|
message: entry.message,
|
|
fileName,
|
|
line: 0,
|
|
column: 0,
|
|
path: entry.path,
|
|
}));
|
|
|
|
const externalAtomsFrom = (
|
|
state: LoweringState,
|
|
contexts: readonly ExternalAtomDeclContext[],
|
|
): AtomDefinition[] => contexts.map((context) => {
|
|
const atom: AtomDefinition = {
|
|
id: capabilityId.atom(stringValue(context.stringLiteral())),
|
|
displayName: identifier(context.identifier()),
|
|
};
|
|
declareSymbol(
|
|
state,
|
|
state.atoms,
|
|
atom.displayName,
|
|
atom.id,
|
|
context,
|
|
"external atom",
|
|
);
|
|
return atom;
|
|
});
|
|
|
|
const externalInterfacesFrom = (
|
|
state: LoweringState,
|
|
contexts: readonly ExternalInterfaceDeclContext[],
|
|
): CapabilityExternalInterface[] => contexts.map((context) => {
|
|
const requirement: CapabilityExternalInterface = {
|
|
binding: identifier(context.identifier()),
|
|
revisionId: capabilityId.interfaceRevision(stringValue(context.stringLiteral())),
|
|
};
|
|
declareSymbol(
|
|
state,
|
|
state.interfaces,
|
|
requirement.binding,
|
|
{
|
|
revisionId: requirement.revisionId,
|
|
contractAvailable: false,
|
|
members: new Map(),
|
|
},
|
|
context,
|
|
"external interface",
|
|
);
|
|
return requirement;
|
|
});
|
|
|
|
const resourcePreambleParts = (contexts: readonly ResourcePreambleContext[]) => ({
|
|
imports: contexts.flatMap((context) => context.resourceImportDecl() ?? []),
|
|
atoms: contexts.flatMap((context) => context.externalAtomDecl() ?? []),
|
|
interfaces: contexts.flatMap((context) => context.externalInterfaceDecl() ?? []),
|
|
});
|
|
|
|
const resourceValidationWorkspace = (
|
|
resource: CapabilityResource,
|
|
environment: CapabilityImportEnvironment,
|
|
): WorkspaceRevision => {
|
|
const resolvedInterfaceIds = new Set([
|
|
...(environment.interfaceClosure ?? []).map((revision) => revision.revisionId),
|
|
...[...(environment.interfaces?.values() ?? [])].map((revision) => revision.revisionId),
|
|
...(resource.kind === "interface" ? [resource.revision.revisionId] : []),
|
|
]);
|
|
return ({
|
|
id: capabilityId.workspaceRevision("workspace-revision:resource-validation"),
|
|
workspaceId: capabilityId.workspace("workspace:resource-validation"),
|
|
parentRevisionIds: [],
|
|
sourceRootCommit: resource.revision.source.commit,
|
|
atoms: uniqueAtoms([
|
|
...(environment.externalAtoms ?? []),
|
|
...resource.externalAtoms,
|
|
]),
|
|
sharedAttachments: [],
|
|
interfaceImports: uniqueExactRevisions([
|
|
...(environment.interfaceClosure ?? []),
|
|
...[...(environment.interfaces?.values() ?? [])],
|
|
...(resource.kind === "interface" ? [resource.revision] : []),
|
|
...resource.externalInterfaces
|
|
.filter((requirement) => !resolvedInterfaceIds.has(requirement.revisionId))
|
|
.map((requirement): InterfaceRevision => ({
|
|
interfaceId: capabilityId.interface(`external:${requirement.revisionId}`),
|
|
revisionId: requirement.revisionId,
|
|
displayName: requirement.binding,
|
|
source: {
|
|
repository: `https://external.invalid/${encodeURIComponent(requirement.revisionId)}.git`,
|
|
commit: "0".repeat(40),
|
|
},
|
|
members: [],
|
|
})),
|
|
]),
|
|
packageImports: uniqueExactRevisions([
|
|
...(environment.packageClosure ?? []),
|
|
...[...(environment.packages?.values() ?? [])],
|
|
...(resource.kind === "package" ? [resource.revision] : []),
|
|
]),
|
|
conformances: [],
|
|
constructors: [],
|
|
});
|
|
};
|
|
|
|
export const compileCapabilityResourceSource = (
|
|
sourceText: string,
|
|
options: {
|
|
source: SourceRevision;
|
|
fileName?: string;
|
|
environment?: CapabilityImportEnvironment;
|
|
},
|
|
): CapabilityResourceCompileResult => {
|
|
const fileName = options.fileName ?? "<memory>";
|
|
const environment = options.environment ?? {};
|
|
const { tree, diagnostics } = parseDocument(sourceText, fileName);
|
|
if (diagnostics.length > 0) {
|
|
return { ok: false, diagnostics };
|
|
}
|
|
|
|
const interfaceContext = tree.interfaceResourceDecl();
|
|
const packageContext = tree.packageResourceDecl();
|
|
if (!interfaceContext && !packageContext) {
|
|
return {
|
|
ok: false,
|
|
diagnostics: [{
|
|
phase: "lowering",
|
|
code: "expected-resource",
|
|
message: "Expected a standalone interface or package resource document",
|
|
fileName,
|
|
line: 1,
|
|
column: 0,
|
|
}],
|
|
};
|
|
}
|
|
|
|
const state = newLoweringState(fileName, diagnostics);
|
|
const preamble = resourcePreambleParts(
|
|
(interfaceContext ?? packageContext)!.resourcePreamble(),
|
|
);
|
|
const externalAtoms = externalAtomsFrom(state, preamble.atoms);
|
|
const imports = registerImports(state, preamble.imports, environment);
|
|
const externalInterfaces = externalInterfacesFrom(state, preamble.interfaces);
|
|
|
|
let resource: CapabilityResource;
|
|
if (interfaceContext) {
|
|
const alias = identifier(interfaceContext.identifier());
|
|
declareSymbol(
|
|
state,
|
|
state.interfaces,
|
|
alias,
|
|
{
|
|
revisionId: capabilityId.interfaceRevision(
|
|
stringValue(interfaceContext.stringLiteral(1)),
|
|
),
|
|
contractAvailable: true,
|
|
members: new Map(),
|
|
},
|
|
interfaceContext,
|
|
"interface",
|
|
);
|
|
resource = {
|
|
kind: "interface",
|
|
imports,
|
|
externalAtoms,
|
|
externalInterfaces,
|
|
revision: lowerInterface(state, interfaceContext, options.source),
|
|
};
|
|
} else {
|
|
const context = packageContext!;
|
|
const alias = identifier(context.identifier());
|
|
declareSymbol(
|
|
state,
|
|
state.packages,
|
|
alias,
|
|
{
|
|
revisionId: capabilityId.packageRevision(
|
|
stringValue(context.stringLiteral(1)),
|
|
),
|
|
exports: new Map(),
|
|
},
|
|
context,
|
|
"package",
|
|
);
|
|
resource = {
|
|
kind: "package",
|
|
imports,
|
|
externalAtoms,
|
|
externalInterfaces,
|
|
revision: lowerPackage(state, context, options.source),
|
|
};
|
|
}
|
|
|
|
if (diagnostics.length > 0) {
|
|
return { ok: false, diagnostics };
|
|
}
|
|
const compiled = compileWorkspaceRevision(
|
|
resourceValidationWorkspace(resource, environment),
|
|
);
|
|
if (!compiled.ok) {
|
|
return {
|
|
ok: false,
|
|
diagnostics: validationDiagnostics(fileName, compiled.issues),
|
|
};
|
|
}
|
|
return { ok: true, resource, diagnostics: [] };
|
|
};
|
|
|
|
export const compileCapabilitySource = (
|
|
source: string,
|
|
fileName = "<memory>",
|
|
environment: CapabilityImportEnvironment = {},
|
|
): CapabilitySourceCompileResult => {
|
|
const { tree, diagnostics } = parseDocument(source, fileName);
|
|
if (diagnostics.length > 0) {
|
|
return { ok: false, diagnostics };
|
|
}
|
|
const workspaceContext = tree.workspaceDecl();
|
|
if (!workspaceContext) {
|
|
return {
|
|
ok: false,
|
|
diagnostics: [{
|
|
phase: "lowering",
|
|
code: "expected-workspace",
|
|
message: "Expected a workspace document",
|
|
fileName,
|
|
line: 1,
|
|
column: 0,
|
|
}],
|
|
};
|
|
}
|
|
|
|
const state = newLoweringState(fileName, diagnostics);
|
|
for (const item of workspaceContext.workspaceItem()) {
|
|
if (item.sourceImportDecl()) loweringIssue(state, item, "unresolved-source-import",
|
|
"Local imports require the workspace repository compiler");
|
|
}
|
|
if (diagnostics.length) return { ok: false, diagnostics };
|
|
const lowered = lowerWorkspace(state, workspaceContext, environment);
|
|
if (diagnostics.length > 0) {
|
|
return { ok: false, diagnostics };
|
|
}
|
|
const compiled = compileWorkspaceRevision(lowered.workspace);
|
|
if (!compiled.ok) {
|
|
return {
|
|
ok: false,
|
|
diagnostics: validationDiagnostics(fileName, compiled.issues),
|
|
};
|
|
}
|
|
return {
|
|
ok: true,
|
|
workspace: lowered.workspace,
|
|
plan: compiled.plan,
|
|
imports: lowered.imports,
|
|
diagnostics: [],
|
|
};
|
|
};
|