483bc68a94
Enable evolution by default for source-backed workspaces. Add stable conformance ownership, semantic-major review, candidate typechecking, and durable fenced cutover with explicit migrations and forward recovery. Independently supervise package runtimes so unchanged resource owners keep their processes and connections across cutover. Add scoped invocation authority, resource sessions, and typed callback rebinding. Wire opaque object references through generated bindings and RPCs. Add canonical relationship sets, keyed maps, and ordered lists with scoped transactional mutations, revision checks, and inverse consistency. Support planned cascade deletion, protection, tombstones, and lifecycle foundations. Add journaled structural edits, package/function/migration scaffolding, managed repository creation, and resumable bottom-up dependency pin publication. Document lifetime boundaries, revision pinning, prototype compatibility policy, commands, and deferred work. Validate with 210 tests, user-systemd process/connection continuity, generated-package TypeScript checks, and Nix host/protocol checks. TTL handoff, physical reclamation, general multi-step migrations, and root-systemd migration isolation acceptance remain deferred.
1980 lines
54 KiB
TypeScript
1980 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.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: [],
|
|
};
|
|
};
|