2026-09-25 10:53:19 -07:00
|
|
|
import { createHash } from "node:crypto";
|
2026-09-03 15:46:52 -07:00
|
|
|
import {
|
|
|
|
|
BaseErrorListener,
|
|
|
|
|
CharStream,
|
|
|
|
|
CommonTokenStream,
|
|
|
|
|
type ATNSimulator,
|
|
|
|
|
type ParserRuleContext,
|
|
|
|
|
type RecognitionException,
|
|
|
|
|
type Recognizer,
|
|
|
|
|
type Token,
|
|
|
|
|
} from "antlr4ng";
|
|
|
|
|
import {
|
|
|
|
|
capabilityId,
|
|
|
|
|
cardinalityValueType,
|
|
|
|
|
compileWorkspaceRevision,
|
|
|
|
|
valueType,
|
2026-09-19 21:52:37 -07:00
|
|
|
type ExecutionContract,
|
2026-09-03 15:46:52 -07:00
|
|
|
type AtomId,
|
|
|
|
|
type BoundDependency,
|
|
|
|
|
type CompiledWorkspaceRevision,
|
|
|
|
|
type DependencyPort,
|
|
|
|
|
type EdgeCardinality,
|
|
|
|
|
type EdgeDefinition,
|
|
|
|
|
type EdgeEndpoint,
|
|
|
|
|
type EdgeEndpointConstraint,
|
|
|
|
|
type EdgePrimitive,
|
2026-09-04 21:03:09 -07:00
|
|
|
type EdgeTraversal,
|
2026-09-03 15:46:52 -07:00
|
|
|
type InterfaceMember,
|
|
|
|
|
type InterfaceOperation,
|
|
|
|
|
type InterfaceOperationMode,
|
|
|
|
|
type InterfaceRevision,
|
2026-09-05 12:33:52 -07:00
|
|
|
type AtomDefinition,
|
2026-09-03 15:46:52 -07:00
|
|
|
type PackageExport,
|
|
|
|
|
type PackageReceiverRequirement,
|
|
|
|
|
type PackageRevision,
|
|
|
|
|
type PersistentAttachment,
|
|
|
|
|
type SourceRevision,
|
|
|
|
|
type StatePrimitive,
|
|
|
|
|
type StateSlotDefinition,
|
|
|
|
|
type ValueType,
|
|
|
|
|
type WorkspaceRevision,
|
2026-09-16 00:12:39 -07:00
|
|
|
GenericTypeError,
|
|
|
|
|
TypeSubstitution,
|
|
|
|
|
type ValueTypeExpression,
|
|
|
|
|
type ObjectTypeExpression,
|
|
|
|
|
type GenericPackageExport,
|
|
|
|
|
type GenericDependencyPort,
|
|
|
|
|
specializePackageExport,
|
2026-09-03 15:46:52 -07:00
|
|
|
} from "../capability-model/index.js";
|
2026-09-23 00:01:03 -07:00
|
|
|
import { expandConformanceRules, type ConformanceRule } from "../capability-model/conformance-rules.js";
|
|
|
|
|
import type { ClosedTypeArgument, TypeParameter } from "../capability-model/generics.js";
|
2026-09-16 00:12:39 -07:00
|
|
|
import { GenericSourceTypes } from "./generic-types.js";
|
2026-09-17 14:34:16 -07:00
|
|
|
import {
|
|
|
|
|
defaultQueryBudgets,
|
|
|
|
|
type QueryDeclaration,
|
|
|
|
|
type QueryBudgets,
|
|
|
|
|
type QueryUse,
|
|
|
|
|
type QueryTemplate,
|
|
|
|
|
} from "../query/types.js";
|
|
|
|
|
import { specializeQueryTemplate } from "../query/templates.js";
|
2026-09-03 15:46:52 -07:00
|
|
|
import { QuixosCapabilityLexer } from "./generated/QuixosCapabilityLexer.js";
|
2026-09-15 15:23:24 -07:00
|
|
|
import {
|
|
|
|
|
QuixosCapabilityParser,
|
2026-09-19 21:52:37 -07:00
|
|
|
type ExecutionClauseContext,
|
2026-09-03 15:46:52 -07:00
|
|
|
type AttachmentDeclContext,
|
|
|
|
|
type ConformanceDeclContext,
|
|
|
|
|
type DependencyBindingBlockContext,
|
|
|
|
|
type DependencyPortContext,
|
2026-09-05 12:33:52 -07:00
|
|
|
type DocumentContext,
|
2026-09-03 15:46:52 -07:00
|
|
|
type EdgeDeclContext,
|
|
|
|
|
type EdgeEndpointContext,
|
2026-09-05 12:33:52 -07:00
|
|
|
type ExternalAtomDeclContext,
|
|
|
|
|
type ExternalInterfaceDeclContext,
|
|
|
|
|
type InterfaceResourceDeclContext,
|
2026-09-03 15:46:52 -07:00
|
|
|
type PackageConstructorExportContext,
|
2026-09-05 12:33:52 -07:00
|
|
|
type PackageResourceDeclContext,
|
2026-09-03 15:46:52 -07:00
|
|
|
type PackageFunctionExportContext,
|
|
|
|
|
type PackageOperationExportContext,
|
|
|
|
|
type OperationMemberContext,
|
2026-09-05 12:33:52 -07:00
|
|
|
type ResourceImportDeclContext,
|
|
|
|
|
type ResourcePreambleContext,
|
2026-09-03 15:46:52 -07:00
|
|
|
type StateDeclContext,
|
|
|
|
|
type TargetConstraintContext,
|
|
|
|
|
type ValueMemberContext,
|
|
|
|
|
type ValueTypeContext,
|
|
|
|
|
type RelationshipMemberContext,
|
2026-09-04 21:03:09 -07:00
|
|
|
type RelationshipMaterializationDeclContext,
|
2026-09-03 15:46:52 -07:00
|
|
|
type WorkspaceDeclContext,
|
|
|
|
|
} from "./generated/QuixosCapabilityParser.js";
|
|
|
|
|
|
2026-09-05 12:33:52 -07:00
|
|
|
export type CapabilityResourceKind = "interface" | "package";
|
|
|
|
|
|
2026-09-16 00:12:39 -07:00
|
|
|
const genericLocations = new WeakMap<GenericTypeError, { line: number; column: number }>();
|
|
|
|
|
const atGenericSource = <T>(context: ParserRuleContext, lower: () => T): T => {
|
|
|
|
|
try {
|
|
|
|
|
return lower();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof GenericTypeError && !genericLocations.has(error))
|
|
|
|
|
genericLocations.set(error, { line: context.start?.line ?? 0, column: context.start?.column ?? 0 });
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-05 12:33:52 -07:00
|
|
|
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;
|
2026-09-16 00:12:39 -07:00
|
|
|
specializations?: InterfaceRevision[];
|
2026-09-05 12:33:52 -07:00
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
kind: "package";
|
|
|
|
|
imports: CapabilityResourceImport[];
|
|
|
|
|
externalAtoms: AtomDefinition[];
|
|
|
|
|
externalInterfaces: CapabilityExternalInterface[];
|
|
|
|
|
revision: PackageRevision;
|
2026-09-16 00:12:39 -07:00
|
|
|
specializations?: InterfaceRevision[];
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type CapabilityResourceCompileResult =
|
|
|
|
|
| { ok: true; resource: CapabilityResource; diagnostics: [] }
|
|
|
|
|
| { ok: false; diagnostics: CapabilitySourceDiagnostic[] };
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
export type CapabilitySourceDiagnosticPhase = "syntax" | "lowering" | "validation";
|
2026-09-03 15:46:52 -07:00
|
|
|
|
|
|
|
|
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;
|
2026-09-05 12:33:52 -07:00
|
|
|
imports: CapabilityResourceImport[];
|
2026-09-03 15:46:52 -07:00
|
|
|
diagnostics: [];
|
|
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
ok: false;
|
|
|
|
|
diagnostics: CapabilitySourceDiagnostic[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface InterfaceSymbol {
|
2026-09-16 00:12:39 -07:00
|
|
|
definition?: InterfaceRevision;
|
2026-09-03 15:46:52 -07:00
|
|
|
revisionId: InterfaceRevision["revisionId"];
|
2026-09-05 12:33:52 -07:00
|
|
|
contractAvailable: boolean;
|
2026-09-03 15:46:52 -07:00
|
|
|
members: Map<
|
|
|
|
|
string,
|
|
|
|
|
{
|
2026-09-04 21:03:09 -07:00
|
|
|
memberId: InterfaceMember["id"];
|
2026-09-03 15:46:52 -07:00
|
|
|
operations: Map<string, InterfaceOperation["id"]>;
|
|
|
|
|
}
|
|
|
|
|
>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PackageExportSymbol {
|
|
|
|
|
exportId: PackageExport["id"];
|
|
|
|
|
ports: Map<string, DependencyPort["id"]>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PackageSymbol {
|
2026-09-17 14:34:16 -07:00
|
|
|
queries?: Map<string, string>;
|
2026-09-16 00:12:39 -07:00
|
|
|
definition?: PackageRevision;
|
2026-09-03 15:46:52 -07:00
|
|
|
revisionId: PackageRevision["revisionId"];
|
|
|
|
|
exports: Map<string, PackageExportSymbol>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface AttachmentSymbol {
|
|
|
|
|
attachment: PersistentAttachment;
|
|
|
|
|
projections: Map<string, EdgeEndpoint["projectionId"]>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface LoweringState {
|
2026-09-16 00:12:39 -07:00
|
|
|
types: GenericSourceTypes;
|
2026-09-03 15:46:52 -07:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const loweringIssue = (state: LoweringState, context: ParserRuleContext, code: string, message: string) => {
|
2026-09-03 15:46:52 -07:00
|
|
|
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;
|
2026-09-15 15:23:24 -07:00
|
|
|
const stringValue = (context: { getText(): string } | null) => JSON.parse(text(context)) as string;
|
2026-09-03 15:46:52 -07:00
|
|
|
|
|
|
|
|
const declareSymbol = <Value>(
|
|
|
|
|
state: LoweringState,
|
|
|
|
|
table: Map<string, Value>,
|
|
|
|
|
name: string,
|
|
|
|
|
value: Value,
|
|
|
|
|
context: ParserRuleContext,
|
|
|
|
|
kind: string,
|
|
|
|
|
) => {
|
|
|
|
|
if (table.has(name)) {
|
2026-09-15 15:23:24 -07:00
|
|
|
loweringIssue(state, context, "duplicate-symbol", `Duplicate ${kind} authoring name ${name}`);
|
2026-09-03 15:46:52 -07:00
|
|
|
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) {
|
2026-09-15 15:23:24 -07:00
|
|
|
loweringIssue(state, context, "unknown-symbol", `Unknown ${kind} ${name}`);
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const lowerCardinality = (context: { getText(): string } | null): EdgeCardinality => text(context) as EdgeCardinality;
|
2026-09-03 15:46:52 -07:00
|
|
|
|
|
|
|
|
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());
|
2026-09-16 00:12:39 -07:00
|
|
|
if (context.OBJECT() || context.typeArguments() || name === "Self" || state.types.interfaces.get(name)?.template)
|
|
|
|
|
return state.types.target(context);
|
2026-09-03 15:46:52 -07:00
|
|
|
if (context.ATOM()) {
|
|
|
|
|
const atomId = requireSymbol(state, state.atoms, name, context, "atom");
|
|
|
|
|
return atomId ? { kind: "atom", atomId } : undefined;
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
const symbol = requireSymbol(state, state.interfaces, name, context, "interface");
|
|
|
|
|
return symbol ? { kind: "interface", interfaceRevisionId: symbol.revisionId } : undefined;
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const lowerValueType = (state: LoweringState, context: ValueTypeContext | null): ValueType => {
|
2026-09-03 15:46:52 -07:00
|
|
|
if (!context) {
|
|
|
|
|
throw new Error("Missing value type after a successful parse");
|
|
|
|
|
}
|
2026-09-16 00:12:39 -07:00
|
|
|
if (
|
|
|
|
|
context.REF() ||
|
|
|
|
|
context.typeArguments() ||
|
|
|
|
|
(context.identifier() && !context.ATOM_REF() && !context.INTERFACE_REF())
|
|
|
|
|
)
|
|
|
|
|
return state.types.value(context);
|
|
|
|
|
if (context.INTERFACE_REF() && state.types.interfaces.get(context.identifier()!.getText())?.template)
|
|
|
|
|
return state.types.value(context);
|
|
|
|
|
if ((context.ATOM_REF() || context.INTERFACE_REF()) && context.identifier()?.getText() === "Self")
|
|
|
|
|
return state.types.value(context);
|
2026-09-03 15:46:52 -07:00
|
|
|
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()));
|
|
|
|
|
}
|
2026-09-13 16:30:17 -07:00
|
|
|
if (context.RECORD()) {
|
2026-09-15 15:23:24 -07:00
|
|
|
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) };
|
2026-09-13 16:30:17 -07:00
|
|
|
}
|
2026-09-03 15:46:52 -07:00
|
|
|
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());
|
2026-09-15 15:23:24 -07:00
|
|
|
const symbol = requireSymbol(state, state.interfaces, name, context, "interface");
|
|
|
|
|
return valueType.interfaceRef(symbol?.revisionId ?? capabilityId.interfaceRevision(`unresolved:${name}`));
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-19 21:52:37 -07:00
|
|
|
const lowerSelectedOperations = (
|
|
|
|
|
state: LoweringState,
|
|
|
|
|
context: DependencyPortContext,
|
|
|
|
|
target: InterfaceSymbol | undefined,
|
|
|
|
|
): { operations?: InterfaceOperation["id"][] } => {
|
|
|
|
|
const selected = context.selectedOperations();
|
|
|
|
|
if (!selected) return {};
|
|
|
|
|
const operations = selected.memberOperationRef().flatMap((reference) => {
|
|
|
|
|
const name = identifier(reference.identifier());
|
|
|
|
|
const member = target && requireSymbol(state, target.members, name, reference, "interface member");
|
|
|
|
|
const operation =
|
|
|
|
|
member &&
|
|
|
|
|
requireSymbol(state, member.operations, text(reference.operationName()), reference, "interface operation");
|
|
|
|
|
return operation ? [operation] : [];
|
|
|
|
|
});
|
|
|
|
|
if (new Set(operations).size !== operations.length)
|
|
|
|
|
loweringIssue(state, context, "duplicate-operation-selection", "Interface operation selection contains duplicates");
|
|
|
|
|
return { operations };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const lowerExecution = (context: ExecutionClauseContext | null): { execution?: ExecutionContract } => {
|
|
|
|
|
if (!context) return {};
|
|
|
|
|
return {
|
|
|
|
|
execution: context.REMOTE()
|
|
|
|
|
? { kind: "remote" }
|
|
|
|
|
: {
|
|
|
|
|
kind: context.SNAPSHOT() ? "snapshot" : "transaction",
|
|
|
|
|
targets: context.identifierList()!.identifier().map(identifier),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const valueOperation = (displayName: string, id: InterfaceOperation["id"], value: ValueType): InterfaceOperation => {
|
2026-09-03 15:46:52 -07:00
|
|
|
switch (displayName) {
|
|
|
|
|
case "get":
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
displayName,
|
|
|
|
|
inputType: valueType.unit,
|
|
|
|
|
outputType: value,
|
|
|
|
|
mode: "call",
|
|
|
|
|
};
|
2026-09-19 22:20:55 -07:00
|
|
|
case "edit":
|
|
|
|
|
return { id, displayName, inputType: { kind: "change", value }, outputType: valueType.unit, mode: "call" };
|
2026-09-03 15:46:52 -07:00
|
|
|
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 =
|
2026-09-15 15:23:24 -07:00
|
|
|
target.kind === "atom" ? valueType.atomRef(target.atomId) : valueType.interfaceRef(target.interfaceRevisionId);
|
2026-09-03 15:46:52 -07:00
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const lowerValueMember = (state: LoweringState, context: ValueMemberContext): InterfaceMember => {
|
2026-09-03 15:46:52 -07:00
|
|
|
const memberName = identifier(context.identifier());
|
|
|
|
|
const memberValueType = lowerValueType(state, context.valueType());
|
|
|
|
|
const operations: InterfaceOperation[] = [];
|
|
|
|
|
for (const operation of context.valueMemberOperation()) {
|
|
|
|
|
if (operation.GET()) {
|
2026-09-19 21:52:37 -07:00
|
|
|
operations.push({
|
|
|
|
|
...valueOperation("get", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
|
|
|
|
|
...lowerExecution(operation.executionClause()),
|
|
|
|
|
});
|
2026-09-19 22:20:55 -07:00
|
|
|
} else if (operation.EDIT()) {
|
|
|
|
|
operations.push({
|
|
|
|
|
...valueOperation("edit", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
|
|
|
|
|
...lowerExecution(operation.executionClause()),
|
|
|
|
|
});
|
2026-09-03 15:46:52 -07:00
|
|
|
} else if (operation.SET()) {
|
2026-09-19 21:52:37 -07:00
|
|
|
operations.push({
|
|
|
|
|
...valueOperation("set", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
|
|
|
|
|
...lowerExecution(operation.executionClause()),
|
|
|
|
|
});
|
2026-09-03 15:46:52 -07:00
|
|
|
} else {
|
|
|
|
|
operations.push(
|
2026-09-15 15:23:24 -07:00
|
|
|
valueOperation("watch-start", capabilityId.operation(stringValue(operation.stringLiteral(0))), memberValueType),
|
|
|
|
|
valueOperation("watch-stop", capabilityId.operation(stringValue(operation.stringLiteral(1))), memberValueType),
|
2026-09-03 15:46:52 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
kind: "value",
|
2026-09-17 14:34:16 -07:00
|
|
|
...(context.queryReadContract()
|
|
|
|
|
? {
|
|
|
|
|
queryRead: {
|
2026-09-19 21:52:37 -07:00
|
|
|
execution: context.queryReadContract()!.RPC()
|
|
|
|
|
? ("rpc-permitted" as const)
|
|
|
|
|
: context.queryReadContract()!.PORTABLE()
|
|
|
|
|
? ("portable" as const)
|
|
|
|
|
: ("native" as const),
|
2026-09-17 14:34:16 -07:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
2026-09-03 15:46:52 -07:00
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-09-19 21:52:37 -07:00
|
|
|
if (operation.executionClause())
|
|
|
|
|
Object.assign(operations[operations.length - 1]!, lowerExecution(operation.executionClause()));
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
kind: "relationship",
|
2026-09-17 14:34:16 -07:00
|
|
|
...(context.KEYED() ? { keyType: stringValue(context.stringLiteral(1)) as "string" | "boolean" | "int64" } : {}),
|
2026-09-17 14:34:16 -07:00
|
|
|
...(context.queryReadContract()
|
|
|
|
|
? {
|
|
|
|
|
queryRead: {
|
2026-09-19 21:52:37 -07:00
|
|
|
execution: context.queryReadContract()!.RPC()
|
|
|
|
|
? ("rpc-permitted" as const)
|
|
|
|
|
: context.queryReadContract()!.PORTABLE()
|
|
|
|
|
? ("portable" as const)
|
|
|
|
|
: ("native" as const),
|
2026-09-17 14:34:16 -07:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
2026-09-17 14:34:16 -07:00
|
|
|
id: capabilityId.member(stringValue(context.stringLiteral(0))),
|
2026-09-03 15:46:52 -07:00
|
|
|
displayName: identifier(context.identifier()),
|
|
|
|
|
target,
|
|
|
|
|
cardinality,
|
|
|
|
|
ordered: Boolean(context.ORDERED()),
|
|
|
|
|
operations,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const lowerOperationMember = (state: LoweringState, context: OperationMemberContext): InterfaceMember => {
|
2026-09-03 15:46:52 -07:00
|
|
|
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,
|
2026-09-15 15:23:24 -07:00
|
|
|
operations: [
|
|
|
|
|
{
|
|
|
|
|
id: capabilityId.operation(stringValue(context.stringLiteral(1))),
|
|
|
|
|
displayName: "call",
|
|
|
|
|
inputType,
|
|
|
|
|
outputType,
|
|
|
|
|
mode: "call",
|
2026-09-16 11:25:20 -07:00
|
|
|
...(context.STATIC() ? { scope: "class" as const } : {}),
|
2026-09-19 21:52:37 -07:00
|
|
|
...lowerExecution(context.executionClause()),
|
2026-09-15 15:23:24 -07:00
|
|
|
},
|
|
|
|
|
],
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const lowerInterface = (
|
|
|
|
|
state: LoweringState,
|
2026-09-05 12:33:52 -07:00
|
|
|
context: InterfaceResourceDeclContext,
|
|
|
|
|
source: SourceRevision,
|
2026-09-03 15:46:52 -07:00
|
|
|
): InterfaceRevision => {
|
|
|
|
|
const alias = identifier(context.identifier());
|
2026-09-22 13:16:23 -07:00
|
|
|
const members = context.interfaceMember().flatMap<InterfaceMember>((entry) => {
|
|
|
|
|
const module = entry.moduleMember();
|
|
|
|
|
if (module)
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
kind: "module" as const,
|
|
|
|
|
id: capabilityId.member(stringValue(module.stringLiteral(0))),
|
|
|
|
|
displayName: identifier(module.identifier()),
|
|
|
|
|
contract: stringValue(module.stringLiteral(1)),
|
|
|
|
|
...(module.targetConstraint() ? { subject: state.types.target(module.targetConstraint()!) } : {}),
|
|
|
|
|
operations: [],
|
|
|
|
|
},
|
|
|
|
|
];
|
2026-09-03 15:46:52 -07:00
|
|
|
const value = entry.valueMember();
|
|
|
|
|
if (value) {
|
|
|
|
|
return [lowerValueMember(state, value)];
|
|
|
|
|
}
|
|
|
|
|
const operation = entry.operationMember();
|
|
|
|
|
if (operation) {
|
|
|
|
|
return [lowerOperationMember(state, operation)];
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
const relationship = lowerRelationshipMember(state, entry.relationshipMember()!);
|
2026-09-03 15:46:52 -07:00
|
|
|
return relationship ? [relationship] : [];
|
|
|
|
|
});
|
|
|
|
|
const symbol = state.interfaces.get(alias)!;
|
|
|
|
|
for (const member of members) {
|
|
|
|
|
declareSymbol(
|
|
|
|
|
state,
|
|
|
|
|
symbol.members,
|
|
|
|
|
member.displayName,
|
|
|
|
|
{
|
2026-09-04 21:03:09 -07:00
|
|
|
memberId: member.id,
|
2026-09-15 15:23:24 -07:00
|
|
|
operations: new Map(member.operations.map((operation) => [operation.displayName, operation.id])),
|
2026-09-03 15:46:52 -07:00
|
|
|
},
|
|
|
|
|
context,
|
|
|
|
|
"interface member",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
interfaceId: capabilityId.interface(stringValue(context.stringLiteral(0))),
|
|
|
|
|
revisionId: symbol.revisionId,
|
|
|
|
|
displayName: alias,
|
2026-09-05 12:33:52 -07:00
|
|
|
source,
|
2026-09-03 15:46:52 -07:00
|
|
|
members,
|
2026-09-16 00:12:39 -07:00
|
|
|
...(context.interfaceType().length
|
|
|
|
|
? {
|
|
|
|
|
requiredInterfaces: context
|
|
|
|
|
.interfaceType()
|
|
|
|
|
.map((entry) => new TypeSubstitution(state.types.environment()).application(state.types.interface(entry))),
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const hasSelfIdentifier = (context: ParserRuleContext, state: LoweringState): boolean => {
|
|
|
|
|
if (context.ruleIndex === QuixosCapabilityParser.RULE_identifier && context.getText() === "Self") return true;
|
|
|
|
|
if (context.ruleIndex === QuixosCapabilityParser.RULE_valueType) {
|
|
|
|
|
const aliasName = context.children
|
|
|
|
|
.find((child) => "ruleIndex" in child && child.ruleIndex === QuixosCapabilityParser.RULE_identifier)
|
|
|
|
|
?.getText();
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
const containsSelf = (value: unknown): boolean => {
|
|
|
|
|
if (!value || typeof value !== "object") return false;
|
|
|
|
|
if ("kind" in value && value.kind === "self") return true;
|
|
|
|
|
if ("kind" in value && value.kind === "alias" && "definitionId" in value) {
|
|
|
|
|
const id = String(value.definitionId);
|
|
|
|
|
if (!seen.has(id)) {
|
|
|
|
|
seen.add(id);
|
|
|
|
|
if (containsSelf(state.types.aliases.get(id)?.body)) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Object.values(value).some(containsSelf);
|
|
|
|
|
};
|
|
|
|
|
if (aliasName && containsSelf(state.types.aliases.get(aliasName)?.body)) return true;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
context.ruleIndex === QuixosCapabilityParser.RULE_interfaceType ||
|
|
|
|
|
((context.ruleIndex === QuixosCapabilityParser.RULE_valueType ||
|
|
|
|
|
context.ruleIndex === QuixosCapabilityParser.RULE_targetConstraint) &&
|
|
|
|
|
["interface", "interface-ref"].includes(context.getChild(0)?.getText() ?? ""))
|
|
|
|
|
) {
|
|
|
|
|
const name = context.children
|
|
|
|
|
.find((child) => "ruleIndex" in child && child.ruleIndex === QuixosCapabilityParser.RULE_identifier)
|
|
|
|
|
?.getText();
|
|
|
|
|
if (name && state.types.interfaces.get(name)?.template?.usesSelf) return true;
|
|
|
|
|
}
|
|
|
|
|
return context.children.some((child) => "ruleIndex" in child && hasSelfIdentifier(child as ParserRuleContext, state));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const lowerInterfaceTemplate = (
|
|
|
|
|
state: LoweringState,
|
|
|
|
|
context: InterfaceResourceDeclContext,
|
|
|
|
|
source: SourceRevision,
|
|
|
|
|
): InterfaceRevision => {
|
|
|
|
|
const types = state.types;
|
|
|
|
|
const revisionId = capabilityId.interfaceRevision(stringValue(context.stringLiteral(1)));
|
|
|
|
|
const parameters = types.declareParameters(context.typeParameters(), JSON.stringify(["interface", revisionId]));
|
|
|
|
|
const signature = (
|
|
|
|
|
name: string,
|
|
|
|
|
id: InterfaceOperation["id"],
|
|
|
|
|
input: ValueTypeExpression,
|
|
|
|
|
output: ValueTypeExpression,
|
|
|
|
|
mode: InterfaceOperationMode = "call",
|
|
|
|
|
eventType?: ValueTypeExpression,
|
|
|
|
|
): InterfaceOperation<ValueTypeExpression> => ({
|
|
|
|
|
id,
|
|
|
|
|
displayName: name,
|
|
|
|
|
inputType: input,
|
|
|
|
|
outputType: output,
|
|
|
|
|
mode,
|
|
|
|
|
...(eventType ? { eventType } : {}),
|
|
|
|
|
});
|
|
|
|
|
const members: InterfaceMember<ValueTypeExpression, ObjectTypeExpression>[] = context
|
|
|
|
|
.interfaceMember()
|
|
|
|
|
.map((entry) => {
|
2026-09-22 13:16:23 -07:00
|
|
|
const module = entry.moduleMember();
|
|
|
|
|
if (module)
|
|
|
|
|
return {
|
|
|
|
|
kind: "module" as const,
|
|
|
|
|
id: capabilityId.member(stringValue(module.stringLiteral(0))),
|
|
|
|
|
displayName: identifier(module.identifier()),
|
|
|
|
|
contract: stringValue(module.stringLiteral(1)),
|
|
|
|
|
...(module.targetConstraint() ? { subject: types.targetExpression(module.targetConstraint()!) } : {}),
|
|
|
|
|
operations: [],
|
|
|
|
|
};
|
2026-09-16 00:12:39 -07:00
|
|
|
const value = entry.valueMember();
|
|
|
|
|
if (value) {
|
|
|
|
|
const type = types.expression(value.valueType());
|
|
|
|
|
return {
|
|
|
|
|
kind: "value",
|
2026-09-17 14:34:16 -07:00
|
|
|
...(value.queryReadContract()
|
|
|
|
|
? {
|
|
|
|
|
queryRead: {
|
2026-09-19 21:52:37 -07:00
|
|
|
execution: value.queryReadContract()!.RPC()
|
|
|
|
|
? ("rpc-permitted" as const)
|
|
|
|
|
: value.queryReadContract()!.PORTABLE()
|
|
|
|
|
? ("portable" as const)
|
|
|
|
|
: ("native" as const),
|
2026-09-17 14:34:16 -07:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
2026-09-16 00:12:39 -07:00
|
|
|
id: capabilityId.member(stringValue(value.stringLiteral())),
|
|
|
|
|
displayName: identifier(value.identifier()),
|
|
|
|
|
valueType: type,
|
|
|
|
|
operations: value.valueMemberOperation().flatMap((operation) => {
|
|
|
|
|
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
|
2026-09-19 21:52:37 -07:00
|
|
|
if (operation.GET())
|
|
|
|
|
return [
|
|
|
|
|
{ ...signature("get", id, valueType.unit, type), ...lowerExecution(operation.executionClause()) },
|
|
|
|
|
];
|
2026-09-19 22:20:55 -07:00
|
|
|
if (operation.EDIT())
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
...signature("edit", id, { kind: "change", value: type }, valueType.unit),
|
|
|
|
|
...lowerExecution(operation.executionClause()),
|
|
|
|
|
},
|
|
|
|
|
];
|
2026-09-19 21:52:37 -07:00
|
|
|
if (operation.SET())
|
|
|
|
|
return [
|
|
|
|
|
{ ...signature("set", id, type, valueType.unit), ...lowerExecution(operation.executionClause()) },
|
|
|
|
|
];
|
2026-09-16 00:12:39 -07:00
|
|
|
return [
|
|
|
|
|
signature("watch-start", id, valueType.unit, valueType.watchHandle, "watch-start", type),
|
|
|
|
|
signature(
|
|
|
|
|
"watch-stop",
|
|
|
|
|
capabilityId.operation(stringValue(operation.stringLiteral(1))),
|
|
|
|
|
valueType.watchHandle,
|
|
|
|
|
valueType.unit,
|
|
|
|
|
"watch-stop",
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const operation = entry.operationMember();
|
|
|
|
|
if (operation) {
|
|
|
|
|
const inputType = types.expression(operation.valueType(0)!);
|
|
|
|
|
const outputType = types.expression(operation.valueType(1)!);
|
|
|
|
|
return {
|
|
|
|
|
kind: "operation",
|
|
|
|
|
id: capabilityId.member(stringValue(operation.stringLiteral(0))),
|
|
|
|
|
displayName: identifier(operation.identifier()),
|
|
|
|
|
inputType,
|
|
|
|
|
outputType,
|
|
|
|
|
operations: [
|
2026-09-16 11:25:20 -07:00
|
|
|
{
|
|
|
|
|
...signature(
|
|
|
|
|
"call",
|
|
|
|
|
capabilityId.operation(stringValue(operation.stringLiteral(1))),
|
|
|
|
|
inputType,
|
|
|
|
|
outputType,
|
|
|
|
|
),
|
|
|
|
|
...(operation.STATIC() ? { scope: "class" as const } : {}),
|
2026-09-19 21:52:37 -07:00
|
|
|
...lowerExecution(operation.executionClause()),
|
2026-09-16 11:25:20 -07:00
|
|
|
},
|
2026-09-16 00:12:39 -07:00
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const relationship = entry.relationshipMember()!;
|
|
|
|
|
const target = types.targetExpression(relationship.targetConstraint());
|
|
|
|
|
const targetType: ValueTypeExpression = { kind: "object-ref", expectation: target };
|
|
|
|
|
const cardinality = lowerCardinality(relationship.cardinality());
|
|
|
|
|
const resolved: ValueTypeExpression =
|
|
|
|
|
cardinality === "exactly-one"
|
|
|
|
|
? targetType
|
|
|
|
|
: { kind: cardinality === "optional-one" ? "optional" : "list", value: targetType };
|
|
|
|
|
return {
|
|
|
|
|
kind: "relationship",
|
2026-09-17 14:34:16 -07:00
|
|
|
...(relationship.KEYED()
|
|
|
|
|
? { keyType: stringValue(relationship.stringLiteral(1)) as "string" | "boolean" | "int64" }
|
|
|
|
|
: {}),
|
2026-09-17 14:34:16 -07:00
|
|
|
...(relationship.queryReadContract()
|
|
|
|
|
? {
|
|
|
|
|
queryRead: {
|
2026-09-19 21:52:37 -07:00
|
|
|
execution: relationship.queryReadContract()!.RPC()
|
|
|
|
|
? ("rpc-permitted" as const)
|
|
|
|
|
: relationship.queryReadContract()!.PORTABLE()
|
|
|
|
|
? ("portable" as const)
|
|
|
|
|
: ("native" as const),
|
2026-09-17 14:34:16 -07:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
2026-09-17 14:34:16 -07:00
|
|
|
id: capabilityId.member(stringValue(relationship.stringLiteral(0))),
|
2026-09-16 00:12:39 -07:00
|
|
|
displayName: identifier(relationship.identifier()),
|
|
|
|
|
target,
|
|
|
|
|
cardinality,
|
|
|
|
|
ordered: Boolean(relationship.ORDERED()),
|
|
|
|
|
operations: relationship.relationshipOperation().flatMap((operation) => {
|
|
|
|
|
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
|
2026-09-19 21:52:37 -07:00
|
|
|
if (operation.RESOLVE())
|
|
|
|
|
return [
|
|
|
|
|
{ ...signature("resolve", id, valueType.unit, resolved), ...lowerExecution(operation.executionClause()) },
|
|
|
|
|
];
|
2026-09-16 00:12:39 -07:00
|
|
|
if (operation.CONNECT() || operation.DISCONNECT())
|
2026-09-19 21:52:37 -07:00
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
...signature(operation.CONNECT() ? "connect" : "disconnect", id, targetType, valueType.unit),
|
|
|
|
|
...lowerExecution(operation.executionClause()),
|
|
|
|
|
},
|
|
|
|
|
];
|
2026-09-16 00:12:39 -07:00
|
|
|
return [
|
|
|
|
|
signature("watch-start", id, valueType.unit, valueType.watchHandle, "watch-start", resolved),
|
|
|
|
|
signature(
|
|
|
|
|
"watch-stop",
|
|
|
|
|
capabilityId.operation(stringValue(operation.stringLiteral(1))),
|
|
|
|
|
valueType.watchHandle,
|
|
|
|
|
valueType.unit,
|
|
|
|
|
"watch-stop",
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
const requires = context.interfaceType().map((requirement) => types.interface(requirement));
|
|
|
|
|
const memberIds = new Set<string>();
|
|
|
|
|
const memberNames = new Set<string>();
|
|
|
|
|
const operationIds = new Set<string>();
|
|
|
|
|
for (const member of members) {
|
|
|
|
|
if (!member.id || memberIds.has(member.id) || memberNames.has(member.displayName))
|
|
|
|
|
throw new GenericTypeError(
|
|
|
|
|
"duplicate-interface-member",
|
|
|
|
|
revisionId,
|
|
|
|
|
`Invalid or duplicate member ${member.displayName}`,
|
|
|
|
|
);
|
|
|
|
|
memberIds.add(member.id);
|
|
|
|
|
memberNames.add(member.displayName);
|
|
|
|
|
const operationNames = new Set<string>();
|
|
|
|
|
for (const operation of member.operations) {
|
|
|
|
|
if (!operation.id || operationIds.has(operation.id) || operationNames.has(operation.displayName))
|
|
|
|
|
throw new GenericTypeError(
|
|
|
|
|
"duplicate-interface-operation",
|
|
|
|
|
revisionId,
|
|
|
|
|
`Invalid or duplicate operation ${member.displayName}.${operation.displayName}`,
|
|
|
|
|
);
|
|
|
|
|
operationIds.add(operation.id);
|
|
|
|
|
operationNames.add(operation.displayName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const hasSelf = (value: unknown): boolean =>
|
|
|
|
|
value !== null &&
|
|
|
|
|
typeof value === "object" &&
|
|
|
|
|
(("kind" in value && value.kind === "self") || Object.values(value).some(hasSelf));
|
|
|
|
|
const aliases = [...types.aliases.values()];
|
|
|
|
|
types.parameters = new Map();
|
|
|
|
|
return {
|
|
|
|
|
interfaceId: capabilityId.interface(stringValue(context.stringLiteral(0))),
|
|
|
|
|
revisionId,
|
|
|
|
|
displayName: identifier(context.identifier()),
|
|
|
|
|
source,
|
|
|
|
|
members: [],
|
|
|
|
|
template: { parameters, members, requires, aliases, usesSelf: hasSelf([parameters, members, requires, aliases]) },
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-17 14:34:16 -07:00
|
|
|
const lowerQueryReference = (
|
|
|
|
|
state: LoweringState,
|
|
|
|
|
packageName: string,
|
|
|
|
|
queryName: string,
|
|
|
|
|
context: ParserRuleContext,
|
|
|
|
|
) => {
|
|
|
|
|
const pkg = requireSymbol(state, state.packages, packageName, context, "package");
|
|
|
|
|
const queryId =
|
|
|
|
|
pkg?.queries?.get(queryName) ?? pkg?.definition?.queries?.find((query) => query.displayName === queryName)?.id;
|
|
|
|
|
if (!pkg || !queryId) {
|
|
|
|
|
loweringIssue(state, context, "unknown-query", `Unknown query export ${packageName}.${queryName}`);
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
return { packageRevisionId: pkg.revisionId, queryId };
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const lowerDependencyPort = (state: LoweringState, context: DependencyPortContext): DependencyPort | undefined => {
|
2026-09-03 15:46:52 -07:00
|
|
|
const name = identifier(context.identifier(0));
|
|
|
|
|
const id = capabilityId.dependencyPort(stringValue(context.stringLiteral()));
|
2026-09-18 18:54:09 -07:00
|
|
|
if (context.REFLECTION())
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
displayName: name,
|
|
|
|
|
requirement: { kind: "reflection", selectors: context.identifierList()!.identifier().map(identifier) },
|
|
|
|
|
};
|
2026-09-17 14:34:16 -07:00
|
|
|
if (context.QUERY()) {
|
|
|
|
|
const query = lowerQueryReference(
|
|
|
|
|
state,
|
|
|
|
|
identifier(context.identifier(1)),
|
|
|
|
|
identifier(context.identifier(2)),
|
|
|
|
|
context,
|
|
|
|
|
);
|
|
|
|
|
return query ? { id, displayName: name, requirement: { kind: "query", ...query } } : undefined;
|
|
|
|
|
}
|
2026-09-03 15:46:52 -07:00
|
|
|
if (context.STATE()) {
|
|
|
|
|
const primitives = context
|
|
|
|
|
.primitiveList()!
|
|
|
|
|
.primitive()
|
|
|
|
|
.map((entry) => text(entry));
|
2026-09-19 22:20:55 -07:00
|
|
|
const invalid = primitives.filter(
|
|
|
|
|
(entry) => !["read", "write", "edit", "watch-start", "watch-stop"].includes(entry),
|
|
|
|
|
);
|
2026-09-03 15:46:52 -07:00
|
|
|
if (invalid.length > 0) {
|
2026-09-15 15:23:24 -07:00
|
|
|
loweringIssue(state, context, "invalid-port-primitive", `State port cannot request ${invalid.join(", ")}`);
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
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(
|
2026-09-15 15:23:24 -07:00
|
|
|
(entry) => !["resolve", "connect", "disconnect", "watch-start", "watch-stop"].includes(entry),
|
2026-09-03 15:46:52 -07:00
|
|
|
);
|
|
|
|
|
if (invalid.length > 0) {
|
2026-09-15 15:23:24 -07:00
|
|
|
loweringIssue(state, context, "invalid-port-primitive", `Edge port cannot request ${invalid.join(", ")}`);
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
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()) {
|
2026-09-16 00:12:39 -07:00
|
|
|
let target = requireSymbol(state, state.interfaces, targetName, context, "interface");
|
|
|
|
|
if (target && (context.typeArguments() || target.definition?.template)) {
|
|
|
|
|
const expression = state.types.interfaceByName(targetName, context.typeArguments());
|
|
|
|
|
const id = new TypeSubstitution(state.types.environment()).application(expression);
|
|
|
|
|
target = interfaceSymbolFor(state.types.definitions.get(id)!);
|
|
|
|
|
}
|
2026-09-05 12:33:52 -07:00
|
|
|
if (target && !target.contractAvailable) {
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
context,
|
|
|
|
|
"nominal-interface-port",
|
|
|
|
|
`Interface port ${name} needs the full ${targetName} contract; use import interface ${targetName}`,
|
|
|
|
|
);
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
2026-09-03 15:46:52 -07:00
|
|
|
return target
|
|
|
|
|
? {
|
|
|
|
|
id,
|
|
|
|
|
displayName: name,
|
|
|
|
|
requirement: {
|
2026-09-05 12:33:52 -07:00
|
|
|
kind: "interface",
|
2026-09-03 15:46:52 -07:00
|
|
|
interfaceRevisionId: target.revisionId,
|
2026-09-19 21:52:37 -07:00
|
|
|
...lowerSelectedOperations(state, context, target),
|
2026-09-20 22:05:59 -07:00
|
|
|
...(context.FROM() ? { receiver: "input" as const } : {}),
|
2026-09-03 15:46:52 -07:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
}
|
|
|
|
|
const atomId = requireSymbol(state, state.atoms, targetName, context, "atom");
|
2026-09-19 22:09:55 -07:00
|
|
|
if (context.CREATION())
|
|
|
|
|
return atomId
|
|
|
|
|
? {
|
|
|
|
|
id,
|
|
|
|
|
displayName: name,
|
|
|
|
|
requirement: { kind: "creation", atomId, inputType: lowerValueType(state, context.valueType()) },
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
2026-09-03 15:46:52 -07:00
|
|
|
return atomId
|
|
|
|
|
? {
|
|
|
|
|
id,
|
|
|
|
|
displayName: name,
|
2026-09-15 15:23:24 -07:00
|
|
|
requirement: {
|
|
|
|
|
kind: "constructor",
|
|
|
|
|
atomId,
|
|
|
|
|
...(context.valueType() ? { inputType: lowerValueType(state, context.valueType()) } : {}),
|
|
|
|
|
},
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const lowerDependencyPorts = (
|
|
|
|
|
state: LoweringState,
|
|
|
|
|
context: { dependencyBlock(): ReturnType<PackageOperationExportContext["dependencyBlock"]> },
|
|
|
|
|
) => {
|
|
|
|
|
const block = context.dependencyBlock();
|
|
|
|
|
if (!block) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
return block.dependencyPort().flatMap((entry) => {
|
|
|
|
|
const port = lowerDependencyPort(state, entry);
|
|
|
|
|
return port ? [port] : [];
|
|
|
|
|
});
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const lowerReceiver = (
|
|
|
|
|
state: LoweringState,
|
2026-09-15 15:23:24 -07:00
|
|
|
context: PackageOperationExportContext["receiverRequirement"] extends () => infer Result ? Result : never,
|
2026-09-03 15:46:52 -07:00
|
|
|
): PackageReceiverRequirement => {
|
2026-09-16 00:12:39 -07:00
|
|
|
if (context.OBJECT())
|
|
|
|
|
throw new GenericTypeError(
|
|
|
|
|
"unbound-parameter",
|
|
|
|
|
context.getText(),
|
|
|
|
|
"An object-parameter receiver requires a generic operation declaration",
|
|
|
|
|
);
|
2026-09-03 15:46:52 -07:00
|
|
|
if (context.ANY()) {
|
|
|
|
|
return { kind: "any-object" };
|
|
|
|
|
}
|
|
|
|
|
if (context.ATOM()) {
|
|
|
|
|
const name = identifier(context.identifier());
|
|
|
|
|
return {
|
|
|
|
|
kind: "exact-atom",
|
2026-09-15 15:23:24 -07:00
|
|
|
atomId: requireSymbol(state, state.atoms, name, context, "atom") ?? capabilityId.atom(`unresolved:${name}`),
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
kind: "all-interfaces",
|
2026-09-16 00:12:39 -07:00
|
|
|
interfaceRevisionIds: context.interfaceType().map((entry) => {
|
|
|
|
|
const name = identifier(entry.identifier());
|
|
|
|
|
if (entry.typeArguments() || state.types.interfaces.get(name)?.template)
|
|
|
|
|
return new TypeSubstitution(state.types.environment()).application(state.types.interface(entry));
|
2026-09-03 15:46:52 -07:00
|
|
|
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) {
|
2026-09-15 15:23:24 -07:00
|
|
|
declareSymbol(state, portSymbols, port.displayName, port.id, context, "dependency port");
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
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,
|
2026-09-19 21:52:37 -07:00
|
|
|
...lowerExecution(context.executionClause()),
|
2026-09-03 15:46:52 -07:00
|
|
|
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",
|
2026-09-19 21:52:37 -07:00
|
|
|
...lowerExecution(context.executionClause()),
|
2026-09-03 15:46:52 -07:00
|
|
|
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 =
|
2026-09-15 15:23:24 -07:00
|
|
|
requireSymbol(state, state.atoms, atomName, context, "atom") ?? capabilityId.atom(`unresolved:${atomName}`);
|
2026-09-03 15:46:52 -07:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-16 00:12:39 -07:00
|
|
|
const lowerGenericPackageExport = (
|
|
|
|
|
state: LoweringState,
|
|
|
|
|
context: PackageOperationExportContext | PackageFunctionExportContext,
|
|
|
|
|
): GenericPackageExport => {
|
|
|
|
|
const previous = state.types.parameters;
|
|
|
|
|
state.types.parameters = new Map();
|
|
|
|
|
const id = capabilityId.packageExport(stringValue(context.stringLiteral()));
|
|
|
|
|
try {
|
|
|
|
|
const parameters = state.types.declareParameters(context.typeParameters(), JSON.stringify(["export", id]));
|
|
|
|
|
const dependencyPorts = (context.dependencyBlock()?.dependencyPort() ?? []).map((port): GenericDependencyPort => {
|
|
|
|
|
const base = {
|
|
|
|
|
id: capabilityId.dependencyPort(stringValue(port.stringLiteral())),
|
|
|
|
|
displayName: identifier(port.identifier(0)),
|
|
|
|
|
};
|
|
|
|
|
const primitives =
|
|
|
|
|
port
|
|
|
|
|
.primitiveList()
|
|
|
|
|
?.primitive()
|
|
|
|
|
.map((item) => text(item)) ?? [];
|
2026-09-18 18:54:09 -07:00
|
|
|
if (port.REFLECTION())
|
|
|
|
|
return {
|
|
|
|
|
...base,
|
|
|
|
|
requirement: { kind: "reflection", selectors: port.identifierList()!.identifier().map(identifier) },
|
|
|
|
|
};
|
2026-09-17 14:34:16 -07:00
|
|
|
if (port.QUERY()) {
|
|
|
|
|
const query = lowerQueryReference(state, identifier(port.identifier(1)), identifier(port.identifier(2)), port);
|
|
|
|
|
if (!query)
|
|
|
|
|
throw new GenericTypeError(
|
|
|
|
|
"unknown-query",
|
|
|
|
|
base.displayName,
|
|
|
|
|
"Query port requires an existing closed query export",
|
|
|
|
|
);
|
|
|
|
|
return { ...base, requirement: { kind: "query", ...query } };
|
|
|
|
|
}
|
2026-09-16 00:12:39 -07:00
|
|
|
if (port.STATE())
|
|
|
|
|
return {
|
|
|
|
|
...base,
|
|
|
|
|
requirement: {
|
|
|
|
|
kind: "state",
|
|
|
|
|
valueType: state.types.expression(port.valueType()!),
|
|
|
|
|
primitives: primitives as StatePrimitive[],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
if (port.EDGE())
|
|
|
|
|
return {
|
|
|
|
|
...base,
|
|
|
|
|
requirement: {
|
|
|
|
|
kind: "edge",
|
|
|
|
|
target: state.types.targetExpression(port.targetConstraint()!),
|
|
|
|
|
cardinality: lowerCardinality(port.cardinality()!),
|
|
|
|
|
primitives: primitives as EdgePrimitive[],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
if (port.INTERFACE())
|
|
|
|
|
return {
|
|
|
|
|
...base,
|
|
|
|
|
requirement: {
|
|
|
|
|
kind: "interface",
|
|
|
|
|
application: state.types.interfaceByName(identifier(port.identifier(1)), port.typeArguments()),
|
2026-09-19 21:52:37 -07:00
|
|
|
...lowerSelectedOperations(state, port, state.interfaces.get(identifier(port.identifier(1)))),
|
2026-09-20 22:05:59 -07:00
|
|
|
...(port.FROM() ? { receiver: "input" as const } : {}),
|
2026-09-16 00:12:39 -07:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
const name = identifier(port.identifier(1)),
|
|
|
|
|
parameter = state.types.parameters.get(name);
|
|
|
|
|
const target: ObjectTypeExpression =
|
|
|
|
|
parameter?.kind === "object"
|
|
|
|
|
? { kind: "parameter", parameterId: parameter.id }
|
|
|
|
|
: { kind: "atom", atomId: requireSymbol(state, state.atoms, name, port, "atom")! };
|
|
|
|
|
if (!port.valueType())
|
|
|
|
|
throw new GenericTypeError("missing-constructor-input", id, "Constructor ports require an explicit input type");
|
|
|
|
|
return {
|
|
|
|
|
...base,
|
2026-09-19 22:09:55 -07:00
|
|
|
requirement: {
|
|
|
|
|
kind: port.CREATION() ? "creation" : "constructor",
|
|
|
|
|
target,
|
|
|
|
|
inputType: state.types.expression(port.valueType()!),
|
|
|
|
|
},
|
2026-09-16 00:12:39 -07:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
const operation = "receiverRequirement" in context ? context : undefined;
|
|
|
|
|
const receiver = operation?.receiverRequirement();
|
|
|
|
|
let receiverRequirement: GenericPackageExport["receiverRequirement"] = { kind: "any-object" };
|
|
|
|
|
if (receiver?.ATOM())
|
|
|
|
|
receiverRequirement = {
|
|
|
|
|
kind: "target",
|
|
|
|
|
target: {
|
|
|
|
|
kind: "atom",
|
|
|
|
|
atomId: requireSymbol(state, state.atoms, identifier(receiver.identifier()), receiver, "atom")!,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
if (receiver?.OBJECT()) {
|
|
|
|
|
const name = identifier(receiver.identifier());
|
|
|
|
|
const parameter = state.types.parameters.get(name);
|
|
|
|
|
if (parameter?.kind !== "object")
|
|
|
|
|
throw new GenericTypeError("parameter-kind", id, "Receiver needs an object parameter");
|
|
|
|
|
receiverRequirement = { kind: "target", target: { kind: "parameter", parameterId: parameter.id } };
|
|
|
|
|
}
|
|
|
|
|
if (receiver?.INTERFACES())
|
|
|
|
|
receiverRequirement = {
|
|
|
|
|
kind: "interfaces",
|
|
|
|
|
interfaces: receiver.interfaceType().map((item) => state.types.interface(item)),
|
|
|
|
|
};
|
|
|
|
|
const definition: GenericPackageExport = {
|
|
|
|
|
id,
|
|
|
|
|
displayName: identifier(context.identifier()),
|
|
|
|
|
parameters,
|
|
|
|
|
kind: operation ? "operation" : "function",
|
2026-09-19 21:52:37 -07:00
|
|
|
...lowerExecution(context.executionClause()),
|
2026-09-16 00:12:39 -07:00
|
|
|
inputType: state.types.expression(context.valueType(0)!),
|
|
|
|
|
outputType: state.types.expression(context.valueType(1)!),
|
|
|
|
|
dependencyPorts,
|
|
|
|
|
receiverRequirement,
|
|
|
|
|
...(operation ? { mode: text(operation.operationMode()) as InterfaceOperationMode } : {}),
|
|
|
|
|
...(operation?.eventClause() ? { eventType: state.types.expression(operation.eventClause()!.valueType()) } : {}),
|
|
|
|
|
aliases: [...state.types.aliases.values()],
|
|
|
|
|
};
|
|
|
|
|
const bindSelf = (value: unknown): unknown => {
|
|
|
|
|
if (!value || typeof value !== "object") return value;
|
|
|
|
|
if ("kind" in value && value.kind === "self") {
|
|
|
|
|
if (receiverRequirement.kind !== "target")
|
|
|
|
|
throw new GenericTypeError("unbound-self", id, "Generic Self requires an exact or object-parameter receiver");
|
|
|
|
|
return receiverRequirement.target;
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(value)) return value.map(bindSelf);
|
|
|
|
|
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, bindSelf(child)]));
|
|
|
|
|
};
|
|
|
|
|
return bindSelf(definition) as GenericPackageExport;
|
|
|
|
|
} finally {
|
|
|
|
|
state.types.parameters = previous;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-03 15:46:52 -07:00
|
|
|
const lowerPackage = (
|
|
|
|
|
state: LoweringState,
|
2026-09-05 12:33:52 -07:00
|
|
|
context: PackageResourceDeclContext,
|
|
|
|
|
source: SourceRevision,
|
2026-09-03 15:46:52 -07:00
|
|
|
): PackageRevision => {
|
|
|
|
|
const alias = identifier(context.identifier());
|
2026-09-17 14:34:16 -07:00
|
|
|
state.packages.get(alias)!.queries = new Map(
|
|
|
|
|
context.packageExport().flatMap((entry) => {
|
|
|
|
|
const query = entry.packageQuery();
|
|
|
|
|
const specialization = entry.packageQuerySpecialization();
|
|
|
|
|
return query && !query.typeParameters()
|
|
|
|
|
? [[identifier(query.identifier()), stringValue(query.stringLiteral(0))] as const]
|
|
|
|
|
: specialization
|
|
|
|
|
? [[identifier(specialization.identifier(0)), stringValue(specialization.stringLiteral())] as const]
|
|
|
|
|
: [];
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-09-16 00:12:39 -07:00
|
|
|
const genericExports: GenericPackageExport[] = [];
|
2026-09-22 13:16:23 -07:00
|
|
|
const modules: NonNullable<PackageRevision["modules"]> = [];
|
2026-09-17 14:34:16 -07:00
|
|
|
const queries: QueryDeclaration[] = [];
|
2026-09-17 14:34:16 -07:00
|
|
|
const queryTemplates: QueryTemplate[] = [];
|
2026-09-16 00:12:39 -07:00
|
|
|
const exports = context.packageExport().flatMap((exportContext): PackageExport[] => {
|
2026-09-22 13:16:23 -07:00
|
|
|
const module = exportContext.moduleExport();
|
|
|
|
|
if (module) {
|
|
|
|
|
const interfaceRevisionId = new TypeSubstitution(state.types.environment()).application(
|
|
|
|
|
state.types.interface(module.interfaceType()),
|
|
|
|
|
);
|
|
|
|
|
const contract = state.types.definitions.get(interfaceRevisionId);
|
|
|
|
|
const member = contract?.members.find((m) => m.displayName === identifier(module.identifier(1)));
|
|
|
|
|
if (member?.kind !== "module")
|
|
|
|
|
loweringIssue(state, module, "invalid-module-export", "Module export must implement an imported module member");
|
|
|
|
|
else
|
|
|
|
|
modules.push({
|
|
|
|
|
id: stringValue(module.stringLiteral()),
|
|
|
|
|
displayName: identifier(module.identifier(0)),
|
|
|
|
|
interfaceRevisionId,
|
|
|
|
|
memberId: member.id,
|
|
|
|
|
});
|
2026-09-21 11:23:26 -07:00
|
|
|
return [];
|
|
|
|
|
}
|
2026-09-17 14:34:16 -07:00
|
|
|
if (exportContext.packageQuerySpecialization()) return [];
|
2026-09-17 14:34:16 -07:00
|
|
|
const query = exportContext.packageQuery();
|
|
|
|
|
if (query) {
|
2026-09-22 15:41:27 -07:00
|
|
|
const isFragment = query.FRAGMENT().length > 0;
|
|
|
|
|
if (isFragment && (query.FRAGMENT().length !== 2 || query.typeParameters() || query.WORKSPACE()))
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
query,
|
|
|
|
|
"invalid-query-fragment",
|
|
|
|
|
"Export a fragment on an exact object interface; generic fragments and workspace fragment roots are not supported",
|
|
|
|
|
);
|
2026-09-17 14:34:16 -07:00
|
|
|
const previousParameters = state.types.parameters;
|
|
|
|
|
state.types.parameters = new Map();
|
|
|
|
|
try {
|
|
|
|
|
const parameters = state.types.declareParameters(
|
|
|
|
|
query.typeParameters(),
|
|
|
|
|
`query:${stringValue(query.stringLiteral(0))}`,
|
|
|
|
|
);
|
|
|
|
|
const expressions = new Map<string, ReturnType<GenericSourceTypes["interface"]>>();
|
|
|
|
|
const templateViews: QueryTemplate["views"] = [];
|
|
|
|
|
const templateAllowances: QueryTemplate["allowances"] = [];
|
|
|
|
|
const close = (ctx: import("./generated/QuixosCapabilityParser.js").InterfaceTypeContext) =>
|
|
|
|
|
parameters.length
|
|
|
|
|
? (() => {
|
|
|
|
|
const id = capabilityId.interfaceRevision(`query-expression:${expressions.size}`);
|
|
|
|
|
expressions.set(id, state.types.interface(ctx));
|
|
|
|
|
return id;
|
|
|
|
|
})()
|
|
|
|
|
: new TypeSubstitution(state.types.environment()).application(state.types.interface(ctx));
|
|
|
|
|
const declaration: QueryDeclaration = {
|
|
|
|
|
id: stringValue(query.stringLiteral(0)),
|
|
|
|
|
displayName: identifier(query.identifier()),
|
2026-09-18 14:55:12 -07:00
|
|
|
root: { kind: query.WORKSPACE() ? "workspace-interface" : "object", interface: close(query.interfaceType()) },
|
2026-09-17 14:34:16 -07:00
|
|
|
document: stringValue(query.stringLiteral(1)),
|
2026-09-22 15:41:27 -07:00
|
|
|
operation: isFragment
|
|
|
|
|
? query.stringLiteral(3)
|
|
|
|
|
? stringValue(query.stringLiteral(3))
|
|
|
|
|
: "QuixosFragment"
|
|
|
|
|
: stringValue(query.stringLiteral(2)),
|
|
|
|
|
...(isFragment
|
|
|
|
|
? {
|
|
|
|
|
fragment: {
|
|
|
|
|
name: stringValue(query.stringLiteral(2)),
|
|
|
|
|
...(query.stringLiteral(3) ? { operation: stringValue(query.stringLiteral(3)) } : {}),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
2026-09-17 14:34:16 -07:00
|
|
|
fragments: [],
|
|
|
|
|
views: [],
|
|
|
|
|
allowances: [],
|
|
|
|
|
budgets: { ...defaultQueryBudgets },
|
|
|
|
|
watch: false,
|
|
|
|
|
};
|
|
|
|
|
const budgetNames: Record<string, keyof QueryBudgets> = {
|
|
|
|
|
rows: "rows",
|
|
|
|
|
depth: "depth",
|
|
|
|
|
"result-bytes": "resultBytes",
|
|
|
|
|
candidates: "candidates",
|
|
|
|
|
"rpc-calls": "rpcCalls",
|
|
|
|
|
concurrency: "concurrency",
|
|
|
|
|
"deadline-ms": "deadlineMs",
|
|
|
|
|
};
|
|
|
|
|
const assigned = new Set<string>();
|
|
|
|
|
for (const clause of query.queryClause()) {
|
2026-09-19 21:52:37 -07:00
|
|
|
if (clause.REPLICA() || clause.LOCAL()) {
|
|
|
|
|
if (declaration.placement)
|
|
|
|
|
loweringIssue(state, clause, "duplicate-query-placement", "Declare query placement once");
|
|
|
|
|
else if (clause.REPLICA()) declaration.placement = { kind: "replica" };
|
|
|
|
|
else {
|
|
|
|
|
const name = identifier(clause.identifier(0));
|
|
|
|
|
const sourceQueryId = requireSymbol(
|
|
|
|
|
state,
|
|
|
|
|
state.packages.get(alias)!.queries!,
|
|
|
|
|
name,
|
|
|
|
|
clause,
|
|
|
|
|
"replica query",
|
|
|
|
|
);
|
|
|
|
|
if (sourceQueryId) declaration.placement = { kind: "local", sourceQueryId };
|
|
|
|
|
}
|
2026-09-22 15:41:27 -07:00
|
|
|
} else if (clause.FRAGMENTS()) {
|
|
|
|
|
if (clause.stringLiteral()) declaration.fragments.push(stringValue(clause.stringLiteral()!));
|
|
|
|
|
else {
|
|
|
|
|
const imported = lowerQueryReference(
|
|
|
|
|
state,
|
|
|
|
|
identifier(clause.identifier(0)),
|
|
|
|
|
identifier(clause.identifier(1)),
|
|
|
|
|
clause,
|
|
|
|
|
);
|
|
|
|
|
if (imported) {
|
|
|
|
|
const variables: Record<string, string> = {};
|
|
|
|
|
for (const mapping of clause.fragmentVariable()) {
|
|
|
|
|
const name = identifier(mapping.identifier(0));
|
|
|
|
|
if (Object.hasOwn(variables, name))
|
|
|
|
|
loweringIssue(state, mapping, "duplicate-fragment-variable", `Repeated fragment variable ${name}`);
|
|
|
|
|
variables[name] = identifier(mapping.identifier(1));
|
|
|
|
|
}
|
|
|
|
|
(declaration.importedFragments ??= []).push({
|
|
|
|
|
packageRevisionId: imported.packageRevisionId,
|
|
|
|
|
packageAlias: identifier(clause.identifier(0)),
|
|
|
|
|
fragmentId: imported.queryId,
|
|
|
|
|
variables,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (clause.VIEW()) {
|
2026-09-17 14:34:16 -07:00
|
|
|
const name = identifier(clause.identifier(0));
|
|
|
|
|
if (parameters.length) {
|
|
|
|
|
const parameter = state.types.parameters.get(name);
|
|
|
|
|
const atomId = state.atoms.get(name);
|
|
|
|
|
if (clause.OBJECT() && parameter?.kind === "object")
|
|
|
|
|
templateViews.push({
|
|
|
|
|
target: { kind: "parameter", parameterId: parameter.id },
|
|
|
|
|
interface: state.types.interface(clause.interfaceType()!),
|
|
|
|
|
});
|
|
|
|
|
else if (!clause.OBJECT() && atomId)
|
|
|
|
|
templateViews.push({
|
|
|
|
|
target: { kind: "atom", atomId },
|
|
|
|
|
interface: state.types.interface(clause.interfaceType()!),
|
|
|
|
|
});
|
|
|
|
|
else
|
|
|
|
|
loweringIssue(state, clause, "invalid-query-view", "Expected declared object parameter or atom view");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (clause.OBJECT())
|
|
|
|
|
loweringIssue(state, clause, "invalid-query-view", "Object parameter views require a query template");
|
|
|
|
|
const atomId = requireSymbol(state, state.atoms, name, clause, "atom");
|
|
|
|
|
if (atomId) declaration.views.push({ atomId, interfaceRevisionId: close(clause.interfaceType()!) });
|
|
|
|
|
} else if (clause.MAX()) {
|
|
|
|
|
const name = identifier(clause.identifier(0));
|
|
|
|
|
const key = budgetNames[name],
|
|
|
|
|
n = Number(clause.INTEGER()!.getText());
|
2026-09-19 22:20:55 -07:00
|
|
|
if (!key || assigned.has(name) || !Number.isSafeInteger(n) || n < 1 || n > 0xffffffff)
|
2026-09-17 14:34:16 -07:00
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
clause,
|
|
|
|
|
"invalid-query-budget",
|
2026-09-19 22:20:55 -07:00
|
|
|
`Invalid, duplicate, or out-of-range query budget ${name}`,
|
2026-09-17 14:34:16 -07:00
|
|
|
);
|
|
|
|
|
else {
|
|
|
|
|
declaration.budgets[key] = n;
|
|
|
|
|
assigned.add(name);
|
|
|
|
|
}
|
|
|
|
|
} else if (clause.ALLOW()) {
|
|
|
|
|
if (parameters.length) {
|
|
|
|
|
const use = identifier(clause.identifier(1));
|
|
|
|
|
const reason = stringValue(clause.stringLiteral()!);
|
2026-09-17 18:23:16 -07:00
|
|
|
if (!["select", "predicate", "order", "aggregate", "group", "distinct"].includes(use) || !reason.trim())
|
2026-09-17 14:34:16 -07:00
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
clause,
|
|
|
|
|
"invalid-query-allowance",
|
2026-09-17 18:23:16 -07:00
|
|
|
"Expected select/predicate/order/aggregate/group/distinct and a nonempty reason",
|
2026-09-17 14:34:16 -07:00
|
|
|
);
|
|
|
|
|
templateAllowances.push({
|
|
|
|
|
interface: state.types.interface(clause.interfaceType()!),
|
|
|
|
|
memberName: identifier(clause.identifier(0)),
|
|
|
|
|
uses: [use as QueryUse],
|
|
|
|
|
reason,
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const interfaceRevisionId = close(clause.interfaceType()!);
|
|
|
|
|
const contract = [...state.types.interfaces.values(), ...state.types.applications.values()].find(
|
|
|
|
|
(entry) => entry.revisionId === interfaceRevisionId,
|
2026-09-17 14:34:16 -07:00
|
|
|
);
|
2026-09-17 14:34:16 -07:00
|
|
|
const member = contract?.members.find((entry) => entry.displayName === identifier(clause.identifier(0)));
|
|
|
|
|
const use = identifier(clause.identifier(1));
|
|
|
|
|
const reason = stringValue(clause.stringLiteral()!);
|
2026-09-17 18:23:16 -07:00
|
|
|
if (
|
|
|
|
|
!member ||
|
|
|
|
|
!["select", "predicate", "order", "aggregate", "group", "distinct"].includes(use) ||
|
|
|
|
|
!reason.trim()
|
|
|
|
|
)
|
2026-09-17 14:34:16 -07:00
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
clause,
|
|
|
|
|
"invalid-query-allowance",
|
2026-09-17 18:23:16 -07:00
|
|
|
"Query allowances name an exact member, use (select/predicate/order/aggregate/group/distinct), and nonempty reason",
|
2026-09-17 14:34:16 -07:00
|
|
|
);
|
|
|
|
|
else
|
|
|
|
|
declaration.allowances.push({
|
|
|
|
|
interfaceRevisionId,
|
|
|
|
|
memberId: member.id,
|
|
|
|
|
uses: [use as QueryUse],
|
|
|
|
|
reason,
|
|
|
|
|
});
|
|
|
|
|
} else if (clause.WATCH()) declaration.watch = true;
|
|
|
|
|
else if (clause.POLL()) {
|
|
|
|
|
const intervalMs = Number(clause.INTEGER()!.getText()),
|
|
|
|
|
reason = stringValue(clause.stringLiteral()!);
|
|
|
|
|
if (declaration.polling || !Number.isSafeInteger(intervalMs) || intervalMs < 1000 || !reason.trim())
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
clause,
|
|
|
|
|
"invalid-query-polling",
|
|
|
|
|
"Polling needs one interval of at least 1000ms and a reason",
|
|
|
|
|
);
|
|
|
|
|
else declaration.polling = { intervalMs, reason };
|
2026-09-17 14:34:16 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-17 14:34:16 -07:00
|
|
|
if (
|
|
|
|
|
[...queries, ...queryTemplates.map((entry) => entry.declaration)].some(
|
|
|
|
|
(entry) => entry.id === declaration.id || entry.displayName === declaration.displayName,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
loweringIssue(state, query, "duplicate-query", "Duplicate query name or ID");
|
|
|
|
|
if (parameters.length) {
|
|
|
|
|
const { root, views: _views, allowances: _allowances, ...common } = declaration;
|
|
|
|
|
queryTemplates.push({
|
|
|
|
|
declaration: common,
|
|
|
|
|
parameters,
|
2026-09-18 14:55:12 -07:00
|
|
|
root: { kind: root.kind, interface: expressions.get(root.interface)! },
|
2026-09-17 14:34:16 -07:00
|
|
|
views: templateViews,
|
|
|
|
|
allowances: templateAllowances,
|
|
|
|
|
});
|
2026-09-22 15:41:27 -07:00
|
|
|
} else if (isFragment) {
|
|
|
|
|
if (declaration.placement || declaration.polling || declaration.allowances.length)
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
query,
|
|
|
|
|
"invalid-query-fragment",
|
|
|
|
|
"Fragments are local projections with generated admission; remote allowances, polling and explicit placement are not supported",
|
|
|
|
|
);
|
|
|
|
|
const sourceQueryId = `${declaration.id}:fragment-inputs`;
|
|
|
|
|
queries.push({
|
|
|
|
|
...structuredClone(declaration),
|
|
|
|
|
id: sourceQueryId,
|
|
|
|
|
displayName: `${declaration.displayName}$inputs`,
|
|
|
|
|
placement: { kind: "replica" },
|
|
|
|
|
fragment: { ...declaration.fragment!, admission: true },
|
|
|
|
|
});
|
|
|
|
|
queries.push({ ...declaration, placement: { kind: "local", sourceQueryId } });
|
2026-09-17 14:34:16 -07:00
|
|
|
} else queries.push(declaration);
|
|
|
|
|
return [];
|
|
|
|
|
} finally {
|
|
|
|
|
state.types.parameters = previousParameters;
|
2026-09-17 14:34:16 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-03 15:46:52 -07:00
|
|
|
const operation = exportContext.packageOperationExport();
|
2026-09-16 00:12:39 -07:00
|
|
|
const generic = operation ?? exportContext.packageFunctionExport();
|
|
|
|
|
if (generic?.typeParameters()) {
|
2026-09-20 21:51:43 -07:00
|
|
|
if (generic.acceptanceClause())
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
generic,
|
|
|
|
|
"unsupported-generic-acceptance",
|
|
|
|
|
"Acceptance hooks currently require a closed export",
|
|
|
|
|
);
|
2026-09-16 00:12:39 -07:00
|
|
|
genericExports.push(atGenericSource(generic, () => lowerGenericPackageExport(state, generic)));
|
|
|
|
|
return [];
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
2026-09-16 00:12:39 -07:00
|
|
|
const constructor = exportContext.packageConstructorExport();
|
|
|
|
|
const receiver = operation?.receiverRequirement();
|
|
|
|
|
const selfName = constructor
|
|
|
|
|
? identifier(constructor.identifier(1))
|
|
|
|
|
: receiver?.ATOM()
|
|
|
|
|
? identifier(receiver.identifier())
|
|
|
|
|
: undefined;
|
|
|
|
|
const previousSelf = state.types.self;
|
|
|
|
|
state.types.self = selfName ? state.atoms.get(selfName) : undefined;
|
|
|
|
|
try {
|
|
|
|
|
if (operation) return [lowerPackageOperation(state, alias, operation)];
|
|
|
|
|
const fn = exportContext.packageFunctionExport();
|
|
|
|
|
if (fn) return [lowerPackageFunction(state, alias, fn)];
|
|
|
|
|
return [lowerPackageConstructor(state, alias, constructor!)];
|
|
|
|
|
} finally {
|
|
|
|
|
state.types.self = previousSelf;
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
});
|
2026-09-20 21:51:43 -07:00
|
|
|
// Resolve after all exports have been declared, so checks can follow bodies.
|
|
|
|
|
for (const source of context.packageExport()) {
|
|
|
|
|
const declaration = source.packageOperationExport() ?? source.packageFunctionExport();
|
|
|
|
|
const clause = declaration?.acceptanceClause();
|
|
|
|
|
if (!declaration || !clause || declaration.typeParameters()) continue;
|
|
|
|
|
const entry = exports.find((entry) => entry.displayName === identifier(declaration.identifier()));
|
|
|
|
|
const check = exports.find((entry) => entry.displayName === identifier(clause.identifier()));
|
|
|
|
|
if (!check)
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
clause,
|
|
|
|
|
"unknown-acceptance-export",
|
|
|
|
|
"Acceptance hook must name a closed export in this package",
|
|
|
|
|
);
|
|
|
|
|
else if (entry) entry.acceptanceExportId = check.id;
|
|
|
|
|
}
|
2026-09-17 14:34:16 -07:00
|
|
|
for (const entry of context.packageExport()) {
|
|
|
|
|
const specialized = entry.packageQuerySpecialization();
|
|
|
|
|
if (!specialized) continue;
|
|
|
|
|
const templateName = identifier(specialized.identifier(1));
|
|
|
|
|
const template = queryTemplates.find((entry) => entry.declaration.displayName === templateName);
|
|
|
|
|
if (!template) {
|
|
|
|
|
loweringIssue(state, specialized, "unknown-query-template", `Unknown local query template ${templateName}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const substitution = new TypeSubstitution(state.types.environment());
|
|
|
|
|
const arguments_ = state.types
|
|
|
|
|
.arguments(specialized.typeArguments())
|
|
|
|
|
.map((argument) => substitution.argument(argument));
|
|
|
|
|
const declaration = specializeQueryTemplate(
|
|
|
|
|
template,
|
|
|
|
|
arguments_,
|
|
|
|
|
state.types.environment(),
|
|
|
|
|
() => [...state.types.definitions.values()],
|
|
|
|
|
{ id: stringValue(specialized.stringLiteral()), displayName: identifier(specialized.identifier(0)) },
|
|
|
|
|
);
|
|
|
|
|
if (
|
|
|
|
|
[...queries, ...queryTemplates.map((entry) => entry.declaration)].some(
|
|
|
|
|
(entry) => entry.id === declaration.id || entry.displayName === declaration.displayName,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
loweringIssue(state, specialized, "duplicate-query", "Duplicate query name or ID");
|
|
|
|
|
queries.push(declaration);
|
|
|
|
|
}
|
2026-09-03 15:46:52 -07:00
|
|
|
return {
|
|
|
|
|
packageId: capabilityId.package(stringValue(context.stringLiteral(0))),
|
|
|
|
|
revisionId: state.packages.get(alias)!.revisionId,
|
2026-09-10 18:27:41 -07:00
|
|
|
...(context.INTEGER() ? { semanticMajor: Number(context.INTEGER()!.getText()) } : {}),
|
2026-09-03 15:46:52 -07:00
|
|
|
displayName: alias,
|
2026-09-05 12:33:52 -07:00
|
|
|
source,
|
2026-09-03 15:46:52 -07:00
|
|
|
exports,
|
2026-09-22 13:16:23 -07:00
|
|
|
...(modules.length ? { modules } : {}),
|
2026-09-17 14:34:16 -07:00
|
|
|
...(queries.length ? { queries } : {}),
|
2026-09-17 14:34:16 -07:00
|
|
|
...(queryTemplates.length ? { queryTemplates } : {}),
|
|
|
|
|
...(queries.some((query) => query.argumentRequirements?.length)
|
|
|
|
|
? { argumentRequirements: queries.flatMap((query) => query.argumentRequirements ?? []) }
|
|
|
|
|
: {}),
|
2026-09-16 00:12:39 -07:00
|
|
|
...(genericExports.length ? { genericExports } : {}),
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const lowerState = (state: LoweringState, context: StateDeclContext): StateSlotDefinition => {
|
2026-09-03 15:46:52 -07:00
|
|
|
const atomName = identifier(context.identifier(1));
|
|
|
|
|
const atomId =
|
2026-09-25 10:53:19 -07:00
|
|
|
(atomName === "Self" ? state.types.self : requireSymbol(state, state.atoms, atomName, context, "atom")) ??
|
|
|
|
|
capabilityId.atom(`unresolved:${atomName}`);
|
2026-09-03 15:46:52 -07:00
|
|
|
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()),
|
2026-09-19 22:09:55 -07:00
|
|
|
storagePolicy: policy.REGISTER()
|
|
|
|
|
? { kind: "register" }
|
2026-09-03 15:46:52 -07:00
|
|
|
: {
|
|
|
|
|
kind: "crdt-document",
|
|
|
|
|
updateType: lowerValueType(state, policy.valueType()),
|
|
|
|
|
},
|
|
|
|
|
defaultValue,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const lowerEdgeEndpoint = (state: LoweringState, context: EdgeEndpointContext): EdgeEndpoint | undefined => {
|
2026-09-03 15:46:52 -07:00
|
|
|
const constraint = lowerConstraint(state, context.targetConstraint());
|
|
|
|
|
return constraint
|
|
|
|
|
? {
|
2026-09-15 15:23:24 -07:00
|
|
|
projectionId: capabilityId.edgeProjection(stringValue(context.stringLiteral(0))),
|
2026-09-03 15:46:52 -07:00
|
|
|
displayName: identifier(context.identifier()),
|
|
|
|
|
constraint,
|
|
|
|
|
cardinality: lowerCardinality(context.cardinality()),
|
|
|
|
|
ordered: Boolean(context.ORDERED()),
|
2026-09-10 18:27:41 -07:00
|
|
|
...(context.ON_DELETE() ? { onDelete: stringValue(context.stringLiteral(1)) as EdgeEndpoint["onDelete"] } : {}),
|
|
|
|
|
...(context.RETAIN_OTHER() ? { retainOther: true } : {}),
|
2026-09-15 15:23:24 -07:00
|
|
|
...(context.KEYED()
|
|
|
|
|
? { keyType: stringValue(context.stringLiteral(context.ON_DELETE() ? 2 : 1)) as EdgeEndpoint["keyType"] }
|
|
|
|
|
: {}),
|
|
|
|
|
...(context.PUBLIC_TRAVERSAL() ? { publicTraversal: true } : {}),
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const lowerEdge = (state: LoweringState, context: EdgeDeclContext): EdgeDefinition | undefined => {
|
|
|
|
|
const endpoints = context.edgeEndpoint().flatMap((entry) => {
|
|
|
|
|
const endpoint = lowerEdgeEndpoint(state, entry);
|
|
|
|
|
return endpoint ? [endpoint] : [];
|
|
|
|
|
});
|
2026-09-03 15:46:52 -07:00
|
|
|
if (endpoints.length !== 2) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
kind: "edge",
|
|
|
|
|
id: capabilityId.edgeType(stringValue(context.stringLiteral())),
|
|
|
|
|
displayName: identifier(context.identifier()),
|
|
|
|
|
endpoints: [endpoints[0]!, endpoints[1]!],
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const lowerAttachment = (state: LoweringState, context: AttachmentDeclContext): PersistentAttachment | undefined => {
|
2026-09-03 15:46:52 -07:00
|
|
|
const stateContext = context.stateDecl();
|
2026-09-15 15:23:24 -07:00
|
|
|
return stateContext ? lowerState(state, stateContext) : lowerEdge(state, context.edgeDecl()!);
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const registerAttachment = (state: LoweringState, context: AttachmentDeclContext, attachment: PersistentAttachment) => {
|
2026-09-03 15:46:52 -07:00
|
|
|
const stateContext = context.stateDecl();
|
2026-09-15 15:23:24 -07:00
|
|
|
const name = identifier(stateContext ? stateContext.identifier(0) : context.edgeDecl()!.identifier());
|
2026-09-03 15:46:52 -07:00
|
|
|
const projections = new Map<string, EdgeEndpoint["projectionId"]>();
|
|
|
|
|
if (attachment.kind === "edge") {
|
|
|
|
|
for (const endpoint of attachment.endpoints) {
|
2026-09-15 15:23:24 -07:00
|
|
|
declareSymbol(state, projections, endpoint.displayName, endpoint.projectionId, context, "edge projection");
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
declareSymbol(state, state.attachments, name, { attachment, projections }, context, "attachment");
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const findOperationId = (
|
|
|
|
|
state: LoweringState,
|
|
|
|
|
interfaceAlias: string,
|
|
|
|
|
memberName: string,
|
|
|
|
|
operationName: string,
|
|
|
|
|
context: ParserRuleContext,
|
|
|
|
|
) => {
|
2026-09-15 15:23:24 -07:00
|
|
|
const interfaceSymbol = requireSymbol(state, state.interfaces, interfaceAlias, context, "interface");
|
2026-09-03 15:46:52 -07:00
|
|
|
const member = interfaceSymbol
|
2026-09-15 15:23:24 -07:00
|
|
|
? requireSymbol(state, interfaceSymbol.members, memberName, context, `member on ${interfaceAlias}`)
|
2026-09-03 15:46:52 -07:00
|
|
|
: undefined;
|
|
|
|
|
return member
|
2026-09-15 15:23:24 -07:00
|
|
|
? requireSymbol(state, member.operations, operationName, context, `operation on ${interfaceAlias}.${memberName}`)
|
2026-09-03 15:46:52 -07:00
|
|
|
: 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));
|
2026-09-15 15:23:24 -07:00
|
|
|
const portId = requireSymbol(state, exportSymbol.ports, portName, entry, "dependency port");
|
2026-09-03 15:46:52 -07:00
|
|
|
if (!portId) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
const traversal = (edgeName: string, projectionName: string): EdgeTraversal | undefined => {
|
|
|
|
|
const attachment = requireSymbol(state, state.attachments, edgeName, entry, "attachment");
|
2026-09-04 21:03:09 -07:00
|
|
|
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}`,
|
|
|
|
|
);
|
2026-09-15 15:23:24 -07:00
|
|
|
return projectionId ? { edgeTypeId: attachment.attachment.id, projectionId } : undefined;
|
2026-09-04 21:03:09 -07:00
|
|
|
};
|
2026-09-18 18:54:09 -07:00
|
|
|
if (entry.REFLECTION())
|
|
|
|
|
return [
|
|
|
|
|
{ portId, binding: { kind: "reflection", selectors: entry.identifierList()!.identifier().map(identifier) } },
|
|
|
|
|
];
|
2026-09-17 14:34:16 -07:00
|
|
|
if (entry.QUERY()) {
|
|
|
|
|
const query = lowerQueryReference(state, identifier(entry.identifier(1)), identifier(entry.identifier(2)), entry);
|
|
|
|
|
const via = entry.VIA() ? traversal(identifier(entry.identifier(3)), identifier(entry.identifier(4))) : undefined;
|
|
|
|
|
return query && (!entry.VIA() || via)
|
2026-09-18 14:55:12 -07:00
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
portId,
|
|
|
|
|
binding: {
|
|
|
|
|
kind: "query",
|
|
|
|
|
...query,
|
|
|
|
|
scope: entry.WORKSPACE() ? "workspace" : "object",
|
|
|
|
|
...(via ? { via } : {}),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]
|
2026-09-17 14:34:16 -07:00
|
|
|
: [];
|
|
|
|
|
}
|
2026-09-03 15:46:52 -07:00
|
|
|
if (entry.STATE()) {
|
|
|
|
|
const attachmentName = identifier(entry.identifier(1));
|
2026-09-15 15:23:24 -07:00
|
|
|
const attachment = requireSymbol(state, state.attachments, attachmentName, entry, "attachment");
|
2026-09-03 15:46:52 -07:00
|
|
|
if (!attachment || attachment.attachment.kind !== "state") {
|
|
|
|
|
if (attachment) {
|
2026-09-15 15:23:24 -07:00
|
|
|
loweringIssue(state, entry, "wrong-attachment-kind", `${attachmentName} is an edge, not state`);
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
const via = entry.VIA() ? traversal(identifier(entry.identifier(2)), identifier(entry.identifier(3))) : undefined;
|
2026-09-04 21:03:09 -07:00
|
|
|
if (entry.VIA() && !via) return [];
|
2026-09-15 15:23:24 -07:00
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
portId,
|
|
|
|
|
binding: { kind: "state", slotId: attachment.attachment.id, ...(via ? { via } : {}) },
|
|
|
|
|
},
|
|
|
|
|
];
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
2026-09-05 12:33:52 -07:00
|
|
|
if (entry.EDGE(0) && !entry.INTERFACE()) {
|
2026-09-03 15:46:52 -07:00
|
|
|
const attachmentName = identifier(entry.identifier(1));
|
|
|
|
|
const projectionName = identifier(entry.identifier(2));
|
2026-09-15 15:23:24 -07:00
|
|
|
const attachment = requireSymbol(state, state.attachments, attachmentName, entry, "attachment");
|
2026-09-03 15:46:52 -07:00
|
|
|
if (!attachment || attachment.attachment.kind !== "edge") {
|
|
|
|
|
if (attachment) {
|
2026-09-15 15:23:24 -07:00
|
|
|
loweringIssue(state, entry, "wrong-attachment-kind", `${attachmentName} is state, not an edge`);
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
const projectionId = requireSymbol(
|
|
|
|
|
state,
|
|
|
|
|
attachment.projections,
|
|
|
|
|
projectionName,
|
|
|
|
|
entry,
|
|
|
|
|
`projection on ${attachmentName}`,
|
|
|
|
|
);
|
2026-09-15 15:23:24 -07:00
|
|
|
const via = entry.VIA() ? traversal(identifier(entry.identifier(3)), identifier(entry.identifier(4))) : undefined;
|
2026-09-04 21:03:09 -07:00
|
|
|
if (entry.VIA() && !via) return [];
|
2026-09-03 15:46:52 -07:00
|
|
|
return projectionId
|
|
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
portId,
|
|
|
|
|
binding: {
|
|
|
|
|
kind: "edge",
|
|
|
|
|
edgeTypeId: attachment.attachment.id,
|
|
|
|
|
projectionId,
|
2026-09-04 21:03:09 -07:00
|
|
|
...(via ? { via } : {}),
|
2026-09-03 15:46:52 -07:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
: [];
|
|
|
|
|
}
|
|
|
|
|
if (entry.INTERFACE()) {
|
|
|
|
|
const interfaceName = identifier(entry.identifier(1));
|
2026-09-16 00:12:39 -07:00
|
|
|
let interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, entry, "interface");
|
|
|
|
|
if (interfaceSymbol && (entry.typeArguments() || interfaceSymbol.definition?.template)) {
|
|
|
|
|
const id = new TypeSubstitution(state.types.environment()).application(
|
|
|
|
|
state.types.interfaceByName(interfaceName, entry.typeArguments()),
|
|
|
|
|
);
|
|
|
|
|
interfaceSymbol = interfaceSymbolFor(state.types.definitions.get(id)!);
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
const via = entry.VIA() ? traversal(identifier(entry.identifier(2)), identifier(entry.identifier(3))) : undefined;
|
2026-09-05 12:33:52 -07:00
|
|
|
if (entry.VIA() && !via) return [];
|
2026-09-03 15:46:52 -07:00
|
|
|
return interfaceSymbol
|
|
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
portId,
|
|
|
|
|
binding: {
|
2026-09-05 12:33:52 -07:00
|
|
|
kind: "interface",
|
2026-09-03 15:46:52 -07:00
|
|
|
interfaceRevisionId: interfaceSymbol.revisionId,
|
2026-09-20 22:05:59 -07:00
|
|
|
...(entry.FROM() ? { receiver: "input" as const } : {}),
|
2026-09-05 12:33:52 -07:00
|
|
|
...(via ? { via } : {}),
|
2026-09-03 15:46:52 -07:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
: [];
|
|
|
|
|
}
|
|
|
|
|
const atomName = identifier(entry.identifier(1));
|
2026-09-25 10:53:19 -07:00
|
|
|
const parameter = state.types.parameters.get(atomName);
|
|
|
|
|
const argument = parameter && state.types.bindings.get(parameter.id);
|
|
|
|
|
const atomId =
|
|
|
|
|
atomName === "Self"
|
|
|
|
|
? state.types.self
|
|
|
|
|
: argument?.kind === "object" && argument.target.kind === "atom"
|
|
|
|
|
? argument.target.atomId
|
|
|
|
|
: parameter
|
|
|
|
|
? undefined
|
|
|
|
|
: requireSymbol(state, state.atoms, atomName, entry, "atom");
|
|
|
|
|
if (!atomId && (parameter || atomName === "Self"))
|
|
|
|
|
loweringIssue(state, entry, "invalid-creation-target", "Creation requires a concrete atom argument");
|
2026-09-19 22:09:55 -07:00
|
|
|
if (entry.CREATION())
|
|
|
|
|
return atomId
|
|
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
portId,
|
|
|
|
|
binding: {
|
|
|
|
|
kind: "creation",
|
|
|
|
|
atomId,
|
|
|
|
|
owners: entry.creationOwnerMapping().map((owner) => ({
|
|
|
|
|
interfaceRevisionId: new TypeSubstitution(state.types.environment()).application(
|
|
|
|
|
state.types.interface(owner.interfaceType()),
|
|
|
|
|
),
|
|
|
|
|
inputPath: owner.identifier().map(identifier),
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
: [];
|
2026-09-15 15:23:24 -07:00
|
|
|
return atomId ? [{ portId, binding: { kind: "constructor", atomId } }] : [];
|
2026-09-03 15:46:52 -07:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-04 21:03:09 -07:00
|
|
|
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`);
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
const projectionId =
|
|
|
|
|
edge?.attachment.kind === "edge"
|
|
|
|
|
? requireSymbol(state, edge.projections, identifier(context.identifier(3)), context, `projection on ${edgeName}`)
|
|
|
|
|
: undefined;
|
2026-09-04 21:03:09 -07:00
|
|
|
return member && constructorAtom && edge?.attachment.kind === "edge" && projectionId
|
|
|
|
|
? {
|
|
|
|
|
memberId: member.memberId,
|
|
|
|
|
constructorAtomId: constructorAtom,
|
|
|
|
|
edgeTypeId: edge.attachment.id,
|
|
|
|
|
constructedProjectionId: projectionId,
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-23 00:01:03 -07:00
|
|
|
const withConformanceTypes = <T>(
|
2026-09-03 15:46:52 -07:00
|
|
|
state: LoweringState,
|
2026-09-23 00:01:03 -07:00
|
|
|
atom: AtomId | undefined,
|
|
|
|
|
parameters: TypeParameter[],
|
|
|
|
|
bindings: Map<string, ClosedTypeArgument>,
|
|
|
|
|
run: () => T,
|
|
|
|
|
): T => {
|
|
|
|
|
const previous = { self: state.types.self, parameters: state.types.parameters, bindings: state.types.bindings };
|
|
|
|
|
state.types.self = atom;
|
|
|
|
|
state.types.parameters = new Map(parameters.map((p) => [p.name, p]));
|
|
|
|
|
state.types.bindings = bindings;
|
|
|
|
|
try {
|
|
|
|
|
return run();
|
|
|
|
|
} finally {
|
|
|
|
|
Object.assign(state.types, previous);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclContext): ConformanceRule | undefined => {
|
|
|
|
|
if (!context.stringLiteral()) {
|
|
|
|
|
loweringIssue(state, context, "missing-rule-id", "Interface conformance rules require a stable ID");
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
return withConformanceTypes(state, undefined, [], new Map(), () => {
|
|
|
|
|
const id = stringValue(context.stringLiteral()!);
|
|
|
|
|
const parameters = state.types.declareParameters(
|
|
|
|
|
context.typeParameters(),
|
|
|
|
|
JSON.stringify(["conformance-rule", id]),
|
|
|
|
|
);
|
|
|
|
|
const source = state.types.interfaceByName(
|
|
|
|
|
identifier(context.identifier(0)),
|
|
|
|
|
context.conformanceSourceArguments()?.typeArguments() ?? null,
|
|
|
|
|
);
|
|
|
|
|
const target = state.types.interfaceByName(identifier(context.identifier(1)), context.typeArguments());
|
|
|
|
|
const targetDefinition = state.types.definitions.get(target.definitionId)!;
|
|
|
|
|
const applications = [source, target];
|
|
|
|
|
const members = targetDefinition.template?.members ?? targetDefinition.members;
|
2026-09-25 10:53:19 -07:00
|
|
|
const privateStorage = new Map<string, "state" | "edge">();
|
|
|
|
|
const privateIds = new Set<string>();
|
2026-09-23 00:01:03 -07:00
|
|
|
for (const item of context.conformanceItem()) {
|
2026-09-25 10:53:19 -07:00
|
|
|
const declaration = item.attachmentDecl();
|
|
|
|
|
if (!declaration) continue;
|
|
|
|
|
const stateDeclaration = declaration.stateDecl();
|
|
|
|
|
const edgeDeclaration = declaration.edgeDecl();
|
|
|
|
|
const name = identifier(stateDeclaration ? stateDeclaration.identifier(0) : edgeDeclaration!.identifier());
|
|
|
|
|
const authoredId = stringValue((stateDeclaration ?? edgeDeclaration)!.stringLiteral());
|
|
|
|
|
if (privateStorage.has(name))
|
|
|
|
|
loweringIssue(state, declaration, "duplicate-symbol", `Duplicate private attachment ${name}`);
|
|
|
|
|
if (privateIds.has(authoredId))
|
|
|
|
|
loweringIssue(state, declaration, "duplicate-attachment", `Duplicate private attachment ID ${authoredId}`);
|
|
|
|
|
privateStorage.set(name, stateDeclaration ? "state" : "edge");
|
|
|
|
|
privateIds.add(authoredId);
|
|
|
|
|
if (stateDeclaration && identifier(stateDeclaration.identifier(1)) !== "Self")
|
|
|
|
|
loweringIssue(state, stateDeclaration, "invalid-rule-storage", "Rule private state must attach to Self");
|
|
|
|
|
if (stateDeclaration) {
|
|
|
|
|
try {
|
|
|
|
|
state.types.expression(stateDeclaration.valueType());
|
|
|
|
|
if (stateDeclaration.storagePolicy().valueType())
|
|
|
|
|
state.types.expression(stateDeclaration.storagePolicy().valueType()!);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof GenericTypeError) loweringIssue(state, stateDeclaration, error.code, error.message);
|
|
|
|
|
else throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-23 00:01:03 -07:00
|
|
|
if (
|
2026-09-25 10:53:19 -07:00
|
|
|
edgeDeclaration &&
|
|
|
|
|
!edgeDeclaration
|
|
|
|
|
.edgeEndpoint()
|
|
|
|
|
.some((endpoint) => identifier(endpoint.targetConstraint().identifier()) === "Self")
|
|
|
|
|
)
|
|
|
|
|
loweringIssue(state, edgeDeclaration, "invalid-rule-storage", "Rule private edge must have a Self endpoint");
|
|
|
|
|
for (const endpoint of edgeDeclaration?.edgeEndpoint() ?? []) {
|
|
|
|
|
try {
|
|
|
|
|
state.types.targetExpression(endpoint.targetConstraint());
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof GenericTypeError) loweringIssue(state, endpoint, error.code, error.message);
|
|
|
|
|
else throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (context.conformanceItem().filter((item) => item.initializationDecl()).length > 1)
|
|
|
|
|
loweringIssue(state, context, "duplicate-initialization", "Declare one initialization mapping per rule owner");
|
|
|
|
|
for (const item of context.conformanceItem()) {
|
|
|
|
|
const field = item.stateFieldBindingDecl();
|
|
|
|
|
if (field) {
|
|
|
|
|
const member = members.find((entry) => entry.displayName === identifier(field.identifier(0)));
|
|
|
|
|
if (member?.kind !== "value" || privateStorage.get(identifier(field.identifier(1))) !== "state")
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
field,
|
|
|
|
|
"invalid-state-field-binding",
|
|
|
|
|
"Field shorthand requires a value member and rule-private state",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const initialization = item.initializationDecl();
|
|
|
|
|
for (const entry of initialization?.initializationField() ?? []) {
|
|
|
|
|
const kind = privateStorage.get(identifier(entry.identifier(1)));
|
|
|
|
|
if (!kind || (entry.STATE() && kind !== "state") || (entry.EDGE() && kind !== "edge"))
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
entry,
|
|
|
|
|
"invalid-initialization",
|
|
|
|
|
"Initialization must name matching rule-private storage",
|
|
|
|
|
);
|
|
|
|
|
if (kind === "edge" && entry.EDGE()) {
|
|
|
|
|
const edge = context
|
|
|
|
|
.conformanceItem()
|
|
|
|
|
.flatMap((candidate) => candidate.attachmentDecl()?.edgeDecl() ?? [])
|
|
|
|
|
.find((candidate) => identifier(candidate.identifier()) === identifier(entry.identifier(1)));
|
|
|
|
|
if (
|
|
|
|
|
!edge
|
|
|
|
|
?.edgeEndpoint()
|
|
|
|
|
.some((endpoint) => identifier(endpoint.identifier()) === identifier(entry.identifier(2)))
|
|
|
|
|
)
|
|
|
|
|
loweringIssue(state, entry, "unknown-symbol", "Unknown rule-private edge projection");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const item of context.conformanceItem()) {
|
|
|
|
|
if (item.relationshipMaterializationDecl()) {
|
|
|
|
|
loweringIssue(state, item, "unsupported-rule-storage", "Rule relationship materialization is not supported");
|
2026-09-23 00:01:03 -07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const module = item.moduleBindingDecl();
|
|
|
|
|
if (module) {
|
|
|
|
|
const pkg = requireSymbol(state, state.packages, identifier(module.identifier(1)), module, "package");
|
|
|
|
|
const member = members.find((m) => m.displayName === identifier(module.identifier(0)));
|
|
|
|
|
if (
|
|
|
|
|
member?.kind !== "module" ||
|
|
|
|
|
!pkg?.definition?.modules?.some((m) => m.displayName === identifier(module.identifier(2)))
|
|
|
|
|
)
|
|
|
|
|
loweringIssue(state, module, "invalid-module-binding", "Expected a module member and imported module export");
|
|
|
|
|
}
|
|
|
|
|
const binding = item.operationBindingDecl();
|
|
|
|
|
if (!binding) continue;
|
|
|
|
|
const ref = binding.memberOperationRef();
|
|
|
|
|
const member = members.find((m) => m.displayName === identifier(ref.identifier()));
|
|
|
|
|
if (!member?.operations.some((op) => op.displayName === text(ref.operationName())))
|
|
|
|
|
loweringIssue(state, ref, "unknown-operation", "Unknown target interface member operation");
|
|
|
|
|
const provider = binding.operationProvider();
|
|
|
|
|
if (provider.INTERFACE()) {
|
|
|
|
|
const application = state.types.interfaceByName(identifier(provider.identifier(0)), provider.typeArguments());
|
|
|
|
|
applications.push(application);
|
|
|
|
|
const definition = state.types.definitions.get(application.definitionId)!;
|
|
|
|
|
const targetRef = provider.memberOperationRef()!;
|
|
|
|
|
if (
|
|
|
|
|
!(definition.template?.members ?? definition.members)
|
|
|
|
|
.find((m) => m.displayName === identifier(targetRef.identifier()))
|
|
|
|
|
?.operations.some((op) => op.displayName === text(targetRef.operationName()))
|
|
|
|
|
)
|
|
|
|
|
loweringIssue(state, provider, "unknown-operation", "Unknown forwarding member operation");
|
|
|
|
|
} else if (provider.PACKAGE()) {
|
|
|
|
|
const pkg = requireSymbol(state, state.packages, identifier(provider.identifier(0)), provider, "package");
|
|
|
|
|
const name = identifier(provider.identifier(1));
|
|
|
|
|
if (pkg && !pkg.exports.has(name))
|
|
|
|
|
loweringIssue(state, provider, "unknown-export", `Unknown package export ${name}`);
|
|
|
|
|
const arguments_ = state.types.arguments(provider.typeArguments());
|
|
|
|
|
const exportDefinition = pkg?.definition?.genericExports?.find((entry) => entry.displayName === name);
|
|
|
|
|
const expected = exportDefinition?.parameters ?? [];
|
|
|
|
|
if (expected.length !== arguments_.length || expected.some((p, i) => p.kind !== arguments_[i]?.kind))
|
|
|
|
|
loweringIssue(state, provider, "parameter-kind", "Package export argument kinds or arity do not match");
|
|
|
|
|
for (const block of [
|
|
|
|
|
provider.dependencyBindingBlock(),
|
|
|
|
|
provider.acceptanceBinding()?.dependencyBindingBlock(),
|
|
|
|
|
]) {
|
|
|
|
|
for (const dependency of block?.dependencyBinding() ?? []) {
|
|
|
|
|
if (dependency.INTERFACE())
|
|
|
|
|
applications.push(
|
|
|
|
|
state.types.interfaceByName(identifier(dependency.identifier(1)), dependency.typeArguments()),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-25 10:53:19 -07:00
|
|
|
} else if (provider.STATE() || provider.EDGE()) {
|
|
|
|
|
const name = identifier(provider.identifier(0));
|
|
|
|
|
const expected = provider.STATE() ? "state" : "edge";
|
2026-09-25 11:04:23 -07:00
|
|
|
// A rule may traverse another owner's public inverse projection, just as
|
|
|
|
|
// a concrete conformance may. Resolve external edges after specialization;
|
|
|
|
|
// ordinary ownership validation still permits only public resolve access.
|
|
|
|
|
if (provider.EDGE() && !privateStorage.has(name)) continue;
|
2026-09-25 10:53:19 -07:00
|
|
|
if (privateStorage.get(name) !== expected)
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
provider,
|
|
|
|
|
"invalid-rule-storage-binding",
|
|
|
|
|
`Rule provider must name private ${expected} ${name}`,
|
|
|
|
|
);
|
|
|
|
|
if (provider.EDGE()) {
|
|
|
|
|
const edge = context
|
|
|
|
|
.conformanceItem()
|
|
|
|
|
.flatMap((candidate) => candidate.attachmentDecl()?.edgeDecl() ?? [])
|
|
|
|
|
.find((candidate) => identifier(candidate.identifier()) === name);
|
|
|
|
|
if (
|
|
|
|
|
!edge
|
|
|
|
|
?.edgeEndpoint()
|
|
|
|
|
.some((endpoint) => identifier(endpoint.identifier()) === identifier(provider.identifier(1)))
|
|
|
|
|
)
|
|
|
|
|
loweringIssue(state, provider, "unknown-symbol", "Unknown rule-private edge projection");
|
|
|
|
|
}
|
2026-09-23 00:01:03 -07:00
|
|
|
} else {
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
provider,
|
|
|
|
|
"unsupported-rule-provider",
|
|
|
|
|
"Rule operations forward an interface or bind a package export",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
state.types.validateTemplate({
|
|
|
|
|
...targetDefinition,
|
|
|
|
|
template: {
|
|
|
|
|
parameters,
|
|
|
|
|
members: [],
|
|
|
|
|
requires: applications,
|
|
|
|
|
usesSelf: false,
|
|
|
|
|
aliases: [...state.types.aliases.values()],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return { id, parameters, source, target };
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const lowerConformanceBody = (
|
|
|
|
|
parent: LoweringState,
|
2026-09-03 15:46:52 -07:00
|
|
|
context: ConformanceDeclContext,
|
|
|
|
|
privateAttachments: PersistentAttachment[],
|
2026-09-23 00:01:03 -07:00
|
|
|
implementingAtom?: AtomId,
|
2026-09-25 10:53:19 -07:00
|
|
|
ruleApplicationId?: string,
|
2026-09-03 15:46:52 -07:00
|
|
|
): WorkspaceRevision["conformances"][number] | undefined => {
|
2026-09-25 10:53:19 -07:00
|
|
|
const state = {
|
|
|
|
|
...parent,
|
|
|
|
|
interfaces: new Map(parent.interfaces),
|
|
|
|
|
attachments: ruleApplicationId ? new Map(parent.attachments) : parent.attachments,
|
|
|
|
|
};
|
2026-09-03 15:46:52 -07:00
|
|
|
const atomName = identifier(context.identifier(0));
|
|
|
|
|
const interfaceName = identifier(context.identifier(1));
|
2026-09-23 00:01:03 -07:00
|
|
|
const atomId = implementingAtom ?? requireSymbol(state, state.atoms, atomName, context, "atom");
|
2026-09-16 00:12:39 -07:00
|
|
|
let interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, context, "interface");
|
2026-09-03 15:46:52 -07:00
|
|
|
if (!atomId || !interfaceSymbol) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
2026-09-25 10:53:19 -07:00
|
|
|
if (ruleApplicationId) {
|
|
|
|
|
privateAttachments = [];
|
|
|
|
|
for (const item of context.conformanceItem()) {
|
|
|
|
|
const declaration = item.attachmentDecl();
|
|
|
|
|
if (!declaration) continue;
|
|
|
|
|
const attachment = lowerAttachment(state, declaration);
|
|
|
|
|
if (!attachment) continue;
|
|
|
|
|
const authoredId = attachment.id;
|
|
|
|
|
const scopedId = createHash("sha256")
|
|
|
|
|
.update(JSON.stringify(["quixos-rule-attachment-v1", ruleApplicationId, authoredId]))
|
|
|
|
|
.digest("hex");
|
|
|
|
|
if (attachment.kind === "state") {
|
|
|
|
|
attachment.id = capabilityId.slot(`rule:sha256:${scopedId}`);
|
|
|
|
|
} else {
|
|
|
|
|
attachment.id = capabilityId.edgeType(`rule:sha256:${scopedId}`);
|
|
|
|
|
for (const endpoint of attachment.endpoints) {
|
|
|
|
|
const projectionHash = createHash("sha256")
|
|
|
|
|
.update(JSON.stringify(["quixos-rule-projection-v1", ruleApplicationId, authoredId, endpoint.projectionId]))
|
|
|
|
|
.digest("hex");
|
|
|
|
|
endpoint.projectionId = capabilityId.edgeProjection(`rule:sha256:${projectionHash}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
privateAttachments.push(attachment);
|
|
|
|
|
// A rule application owns a lexical scope. A same-named attachment in
|
|
|
|
|
// another owner must neither prevent this declaration nor receive bindings.
|
|
|
|
|
const name = identifier(declaration.stateDecl()?.identifier(0) ?? declaration.edgeDecl()!.identifier());
|
|
|
|
|
state.attachments.delete(name);
|
|
|
|
|
registerAttachment(state, declaration, attachment);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-16 00:12:39 -07:00
|
|
|
const originalSymbol = interfaceSymbol;
|
|
|
|
|
if (context.typeArguments() || interfaceSymbol.definition?.template) {
|
|
|
|
|
const previousSelf = state.types.self;
|
|
|
|
|
state.types.self = atomId;
|
|
|
|
|
try {
|
|
|
|
|
const expression = state.types.interfaceByName(interfaceName, context.typeArguments());
|
|
|
|
|
const id = new TypeSubstitution(state.types.environment()).application(expression);
|
|
|
|
|
interfaceSymbol = interfaceSymbolFor(state.types.definitions.get(id)!);
|
|
|
|
|
state.interfaces.set(interfaceName, interfaceSymbol);
|
|
|
|
|
} finally {
|
|
|
|
|
state.types.self = previousSelf;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-22 13:16:23 -07:00
|
|
|
const moduleBindings: NonNullable<WorkspaceRevision["conformances"][number]["moduleBindings"]> = [];
|
|
|
|
|
for (const item of context.conformanceItem()) {
|
|
|
|
|
const binding = item.moduleBindingDecl();
|
|
|
|
|
if (!binding) continue;
|
|
|
|
|
const memberName = identifier(binding.identifier(0));
|
|
|
|
|
const member = interfaceSymbol.definition?.members.find((m) => m.displayName === memberName);
|
|
|
|
|
const pkg = requireSymbol(state, state.packages, identifier(binding.identifier(1)), binding, "package");
|
|
|
|
|
const module = pkg?.definition?.modules?.find((m) => m.displayName === identifier(binding.identifier(2)));
|
|
|
|
|
if (member?.kind !== "module" || !module || !pkg) {
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
binding,
|
|
|
|
|
"invalid-module-binding",
|
|
|
|
|
"Expected a module member bound to an imported module export",
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
moduleBindings.push({ memberId: member.id, packageRevisionId: pkg.revisionId, moduleId: module.id });
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
const operationBindings = context
|
|
|
|
|
.conformanceItem()
|
|
|
|
|
.flatMap<WorkspaceRevision["conformances"][number]["operationBindings"][number]>((item) => {
|
2026-09-16 11:25:20 -07:00
|
|
|
const field = item.stateFieldBindingDecl();
|
|
|
|
|
if (field) {
|
|
|
|
|
const memberName = identifier(field.identifier(0));
|
|
|
|
|
const member = interfaceSymbol.definition?.members.find((entry) => entry.displayName === memberName);
|
|
|
|
|
const attachment = requireSymbol(
|
|
|
|
|
state,
|
|
|
|
|
state.attachments,
|
|
|
|
|
identifier(field.identifier(1)),
|
|
|
|
|
field,
|
|
|
|
|
"attachment",
|
|
|
|
|
);
|
|
|
|
|
if (!member || member.kind !== "value" || attachment?.attachment.kind !== "state") {
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
field,
|
|
|
|
|
"invalid-state-field-binding",
|
|
|
|
|
"Field shorthand requires a value member and a state slot",
|
|
|
|
|
);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
const primitives = {
|
|
|
|
|
get: "read",
|
|
|
|
|
set: "write",
|
2026-09-19 22:20:55 -07:00
|
|
|
edit: "edit",
|
2026-09-16 11:25:20 -07:00
|
|
|
"watch-start": "watch-start",
|
|
|
|
|
"watch-stop": "watch-stop",
|
|
|
|
|
} as const;
|
|
|
|
|
const slotId = attachment.attachment.id;
|
|
|
|
|
return member.operations.map((operation) => ({
|
|
|
|
|
operationId: operation.id,
|
|
|
|
|
binding: {
|
|
|
|
|
kind: "state" as const,
|
|
|
|
|
slotId,
|
|
|
|
|
primitive: primitives[operation.displayName as keyof typeof primitives],
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
const bindingContext = item.operationBindingDecl();
|
|
|
|
|
if (!bindingContext) {
|
2026-09-03 15:46:52 -07:00
|
|
|
return [];
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
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();
|
2026-09-20 22:41:51 -07:00
|
|
|
if (provider.LIFECYCLE()) {
|
|
|
|
|
if (identifier(provider.identifier(0)) !== "delete") {
|
|
|
|
|
loweringIssue(state, provider, "invalid-lifecycle-binding", "Only lifecycle.delete is supported");
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
return [{ operationId, binding: { kind: "lifecycle", primitive: "delete" } }];
|
|
|
|
|
}
|
2026-09-18 16:59:19 -07:00
|
|
|
if (provider.INTERFACE()) {
|
|
|
|
|
const targetName = identifier(provider.identifier(0));
|
|
|
|
|
let target = requireSymbol(state, state.interfaces, targetName, provider, "interface");
|
|
|
|
|
if (!target) return [];
|
|
|
|
|
if (provider.typeArguments() || target.definition?.template) {
|
|
|
|
|
const id = new TypeSubstitution({ ...state.types.environment(), self: atomId }).application(
|
|
|
|
|
state.types.interfaceByName(targetName, provider.typeArguments()),
|
|
|
|
|
);
|
|
|
|
|
target = interfaceSymbolFor(state.types.definitions.get(id)!);
|
|
|
|
|
}
|
|
|
|
|
const ref = provider.memberOperationRef()!;
|
|
|
|
|
const member = target.members.get(identifier(ref.identifier()));
|
|
|
|
|
const targetOperation = member?.operations.get(text(ref.operationName()));
|
|
|
|
|
if (!targetOperation) {
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
provider,
|
|
|
|
|
"unknown-operation",
|
|
|
|
|
"Forwarding target must name an interface member operation",
|
|
|
|
|
);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
operationId,
|
|
|
|
|
binding: { kind: "interface", interfaceRevisionId: target.revisionId, operationId: targetOperation },
|
|
|
|
|
...(bindingContext.stringLiteral() ? { queryReason: stringValue(bindingContext.stringLiteral()!) } : {}),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
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,
|
|
|
|
|
},
|
2026-09-03 15:46:52 -07:00
|
|
|
},
|
2026-09-15 15:23:24 -07:00
|
|
|
];
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
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");
|
2026-09-16 00:12:39 -07:00
|
|
|
let exportSymbol = packageSymbol
|
2026-09-15 15:23:24 -07:00
|
|
|
? requireSymbol(state, packageSymbol.exports, exportName, provider, `export on ${packageName}`)
|
|
|
|
|
: undefined;
|
2026-09-16 00:12:39 -07:00
|
|
|
const generic = packageSymbol?.definition?.genericExports?.find((entry) => entry.displayName === exportName);
|
|
|
|
|
if (packageSymbol?.definition && generic) {
|
|
|
|
|
const arguments_ = state.types
|
|
|
|
|
.arguments(provider.typeArguments())
|
|
|
|
|
.map((entry) => new TypeSubstitution({ ...state.types.environment(), self: atomId }).argument(entry));
|
|
|
|
|
const pkg = packageSymbol.definition;
|
|
|
|
|
const specialized = specializePackageExport(pkg, generic, arguments_, {
|
|
|
|
|
...state.types.environment(),
|
|
|
|
|
implementsInterface: (target, required) => {
|
|
|
|
|
(pkg.argumentRequirements ??= []).push({ target, required });
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
if (!pkg.exports.some((entry) => entry.id === specialized.id)) pkg.exports.push(specialized);
|
|
|
|
|
exportSymbol = {
|
|
|
|
|
exportId: specialized.id,
|
|
|
|
|
ports: new Map(specialized.dependencyPorts.map((entry) => [entry.displayName, entry.id])),
|
|
|
|
|
};
|
|
|
|
|
} else if (provider.typeArguments()) {
|
|
|
|
|
throw new GenericTypeError("type-arity", exportName, "Non-generic export takes no type arguments");
|
|
|
|
|
}
|
2026-09-20 21:51:43 -07:00
|
|
|
const packageExport = packageSymbol?.definition?.exports.find((entry) => entry.id === exportSymbol?.exportId);
|
|
|
|
|
const check = packageSymbol?.definition?.exports.find((entry) => entry.id === packageExport?.acceptanceExportId);
|
|
|
|
|
const acceptanceBlock = provider.acceptanceBinding()?.dependencyBindingBlock();
|
|
|
|
|
if (acceptanceBlock && !check)
|
|
|
|
|
loweringIssue(
|
|
|
|
|
state,
|
|
|
|
|
acceptanceBlock,
|
|
|
|
|
"unexpected-acceptance-binding",
|
|
|
|
|
"Export does not declare an acceptance hook",
|
|
|
|
|
);
|
|
|
|
|
const acceptanceDependencies = check
|
|
|
|
|
? lowerBoundDependencies(state, acceptanceBlock ?? undefined, {
|
|
|
|
|
exportId: check.id,
|
|
|
|
|
ports: new Map(check.dependencyPorts.map((port) => [port.displayName, port.id])),
|
|
|
|
|
})
|
|
|
|
|
: undefined;
|
2026-09-15 15:23:24 -07:00
|
|
|
return packageSymbol && exportSymbol
|
2026-09-03 15:46:52 -07:00
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
operationId,
|
2026-09-17 14:34:16 -07:00
|
|
|
...(bindingContext.stringLiteral() ? { queryReason: stringValue(bindingContext.stringLiteral()!) } : {}),
|
2026-09-03 15:46:52 -07:00
|
|
|
binding: {
|
2026-09-15 15:23:24 -07:00
|
|
|
kind: "package" as const,
|
|
|
|
|
packageRevisionId: packageSymbol.revisionId,
|
|
|
|
|
exportId: exportSymbol.exportId,
|
2026-09-20 21:51:43 -07:00
|
|
|
...(acceptanceDependencies ? { acceptanceDependencies } : {}),
|
2026-09-15 15:23:24 -07:00
|
|
|
dependencies: lowerBoundDependencies(
|
|
|
|
|
state,
|
|
|
|
|
provider.dependencyBindingBlock() ?? undefined,
|
|
|
|
|
exportSymbol,
|
|
|
|
|
),
|
2026-09-03 15:46:52 -07:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
: [];
|
2026-09-15 15:23:24 -07:00
|
|
|
});
|
2026-09-04 21:03:09 -07:00
|
|
|
const relationshipMaterializations = context.conformanceItem().flatMap((item) => {
|
|
|
|
|
const materialization = item.relationshipMaterializationDecl();
|
|
|
|
|
if (!materialization) return [];
|
|
|
|
|
const lowered = lowerRelationshipMaterialization(state, interfaceName, materialization);
|
|
|
|
|
return lowered ? [lowered] : [];
|
|
|
|
|
});
|
2026-09-19 22:09:55 -07:00
|
|
|
const initializers = context
|
|
|
|
|
.conformanceItem()
|
|
|
|
|
.flatMap((item) => (item.initializationDecl() ? [item.initializationDecl()!] : []));
|
|
|
|
|
if (initializers.length > 1)
|
|
|
|
|
loweringIssue(state, context, "duplicate-initialization", "Declare one initialization mapping per owner");
|
|
|
|
|
const initialization = initializers.flatMap((initializer) =>
|
|
|
|
|
initializer.initializationField().flatMap((field): import("../capability-model/types.js").InitializationField[] => {
|
|
|
|
|
const input = identifier(field.identifier(0));
|
|
|
|
|
const attachment = requireSymbol(
|
|
|
|
|
state,
|
|
|
|
|
state.attachments,
|
|
|
|
|
identifier(field.identifier(1)),
|
|
|
|
|
field,
|
|
|
|
|
"initialization attachment",
|
|
|
|
|
);
|
|
|
|
|
if (!attachment) return [];
|
|
|
|
|
if (field.STATE() && attachment.attachment.kind === "state")
|
|
|
|
|
return [{ input, binding: { kind: "state", slotId: attachment.attachment.id } }];
|
|
|
|
|
if (field.EDGE() && attachment.attachment.kind === "edge") {
|
|
|
|
|
const projectionId = requireSymbol(
|
|
|
|
|
state,
|
|
|
|
|
attachment.projections,
|
|
|
|
|
identifier(field.identifier(2)),
|
|
|
|
|
field,
|
|
|
|
|
"initialization projection",
|
|
|
|
|
);
|
|
|
|
|
return projectionId
|
|
|
|
|
? [{ input, binding: { kind: "edge", edgeTypeId: attachment.attachment.id, projectionId } }]
|
|
|
|
|
: [];
|
|
|
|
|
}
|
|
|
|
|
loweringIssue(state, field, "invalid-initialization", "Initialization attachment kind does not match");
|
|
|
|
|
return [];
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-09-16 00:12:39 -07:00
|
|
|
state.interfaces.set(interfaceName, originalSymbol);
|
2026-09-03 15:46:52 -07:00
|
|
|
return {
|
2026-09-19 22:09:55 -07:00
|
|
|
...(initializers.length ? { initialization } : {}),
|
2026-09-03 15:46:52 -07:00
|
|
|
atomId,
|
|
|
|
|
interfaceRevisionId: interfaceSymbol.revisionId,
|
2026-09-10 18:27:41 -07:00
|
|
|
...(context.stringLiteral() ? { id: capabilityId.conformance(stringValue(context.stringLiteral()!)) } : {}),
|
|
|
|
|
...(context.INTEGER() ? { semanticMajor: Number(context.INTEGER()!.getText()) } : {}),
|
2026-09-03 15:46:52 -07:00
|
|
|
privateAttachments,
|
|
|
|
|
operationBindings,
|
2026-09-22 13:16:23 -07:00
|
|
|
...(moduleBindings.length ? { moduleBindings } : {}),
|
2026-09-04 21:03:09 -07:00
|
|
|
relationshipMaterializations,
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-05 12:33:52 -07:00
|
|
|
const interfaceSymbolFor = (revision: InterfaceRevision): InterfaceSymbol => ({
|
2026-09-16 00:12:39 -07:00
|
|
|
definition: revision,
|
2026-09-05 12:33:52 -07:00
|
|
|
revisionId: revision.revisionId,
|
|
|
|
|
contractAvailable: true,
|
2026-09-15 15:23:24 -07:00
|
|
|
members: new Map(
|
2026-09-19 21:52:37 -07:00
|
|
|
(revision.template?.members ?? revision.members).map((member) => [
|
2026-09-15 15:23:24 -07:00
|
|
|
member.displayName,
|
|
|
|
|
{
|
|
|
|
|
memberId: member.id,
|
|
|
|
|
operations: new Map(member.operations.map((operation) => [operation.displayName, operation.id])),
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
),
|
2026-09-05 12:33:52 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const packageSymbolFor = (revision: PackageRevision): PackageSymbol => ({
|
2026-09-16 00:12:39 -07:00
|
|
|
definition: structuredClone(revision),
|
2026-09-05 12:33:52 -07:00
|
|
|
revisionId: revision.revisionId,
|
2026-09-15 15:23:24 -07:00
|
|
|
exports: new Map(
|
2026-09-16 00:12:39 -07:00
|
|
|
[...revision.exports, ...(revision.genericExports ?? [])].map((entry) => [
|
2026-09-15 15:23:24 -07:00
|
|
|
entry.displayName,
|
|
|
|
|
{
|
|
|
|
|
exportId: entry.id,
|
|
|
|
|
ports: new Map(entry.dependencyPorts.map((port) => [port.displayName, port.id])),
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
),
|
2026-09-05 12:33:52 -07:00
|
|
|
});
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const resourceImport = (context: ResourceImportDeclContext): CapabilityResourceImport => ({
|
2026-09-05 12:33:52 -07:00
|
|
|
kind: context.INTERFACE() ? "interface" : "package",
|
|
|
|
|
binding: identifier(context.identifier()),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const registerImports = (
|
|
|
|
|
state: LoweringState,
|
|
|
|
|
contexts: readonly ResourceImportDeclContext[],
|
|
|
|
|
environment: CapabilityImportEnvironment,
|
2026-09-16 00:12:39 -07:00
|
|
|
) => {
|
|
|
|
|
for (const definition of environment.interfaceClosure ?? [])
|
|
|
|
|
state.types.definitions.set(definition.revisionId, definition);
|
|
|
|
|
return contexts.map((context) => {
|
2026-09-15 15:23:24 -07:00
|
|
|
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");
|
2026-09-16 00:12:39 -07:00
|
|
|
state.types.register(imported.binding, revision);
|
2026-09-15 15:23:24 -07:00
|
|
|
}
|
2026-09-05 12:33:52 -07:00
|
|
|
} else {
|
2026-09-15 15:23:24 -07:00
|
|
|
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");
|
|
|
|
|
}
|
2026-09-05 12:33:52 -07:00
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
return imported;
|
|
|
|
|
});
|
2026-09-16 00:12:39 -07:00
|
|
|
};
|
2026-09-05 12:33:52 -07:00
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const uniqueExactRevisions = <
|
|
|
|
|
Revision extends {
|
|
|
|
|
revisionId: string;
|
|
|
|
|
source: SourceRevision;
|
|
|
|
|
},
|
|
|
|
|
>(
|
|
|
|
|
revisions: readonly Revision[],
|
|
|
|
|
): Revision[] => {
|
2026-09-05 12:33:52 -07:00
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-03 15:46:52 -07:00
|
|
|
const lowerWorkspace = (
|
|
|
|
|
state: LoweringState,
|
|
|
|
|
context: WorkspaceDeclContext,
|
2026-09-05 12:33:52 -07:00
|
|
|
environment: CapabilityImportEnvironment,
|
|
|
|
|
): { workspace: WorkspaceRevision; imports: CapabilityResourceImport[] } => {
|
2026-09-03 15:46:52 -07:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-05 12:33:52 -07:00
|
|
|
const imports = registerImports(
|
|
|
|
|
state,
|
|
|
|
|
items.flatMap((item) => item.resourceImportDecl() ?? []),
|
|
|
|
|
environment,
|
|
|
|
|
);
|
|
|
|
|
|
2026-09-03 15:46:52 -07:00
|
|
|
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()),
|
2026-09-15 15:23:24 -07:00
|
|
|
documentation: atom.DOC() ? stringValue(atom.stringLiteral(1)) : undefined,
|
2026-09-03 15:46:52 -07:00
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
: [];
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-16 00:12:39 -07:00
|
|
|
state.types.declareAliases(items.flatMap((item) => item.typeAliasDecl() ?? []));
|
|
|
|
|
|
2026-09-05 12:33:52 -07:00
|
|
|
const interfaceImports = uniqueExactRevisions([
|
|
|
|
|
...(environment.interfaceClosure ?? []),
|
|
|
|
|
...[...(environment.interfaces?.values() ?? [])],
|
|
|
|
|
]);
|
2026-09-03 15:46:52 -07:00
|
|
|
|
2026-09-05 12:33:52 -07:00
|
|
|
const packageImports = uniqueExactRevisions([
|
|
|
|
|
...(environment.packageClosure ?? []),
|
|
|
|
|
...[...(environment.packages?.values() ?? [])],
|
|
|
|
|
]);
|
2026-09-03 15:46:52 -07:00
|
|
|
|
|
|
|
|
const sharedAttachments: PersistentAttachment[] = [];
|
2026-09-15 15:23:24 -07:00
|
|
|
const privateAttachments = new Map<ConformanceDeclContext, PersistentAttachment[]>();
|
2026-09-03 15:46:52 -07:00
|
|
|
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();
|
2026-09-23 00:01:03 -07:00
|
|
|
if (conformance && !state.interfaces.has(identifier(conformance.identifier(0)))) {
|
2026-09-16 00:12:39 -07:00
|
|
|
state.types.self = state.atoms.get(identifier(conformance.identifier(0)));
|
2026-09-03 15:46:52 -07:00
|
|
|
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);
|
2026-09-16 00:12:39 -07:00
|
|
|
state.types.self = undefined;
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 00:01:03 -07:00
|
|
|
const contexts = items.flatMap((item) => item.conformanceDecl() ?? []);
|
|
|
|
|
const rules = new Map<ConformanceRule, ConformanceDeclContext>();
|
|
|
|
|
const direct: ConformanceDeclContext[] = [];
|
|
|
|
|
for (const declaration of contexts) {
|
|
|
|
|
const name = identifier(declaration.identifier(0));
|
|
|
|
|
if (state.interfaces.has(name) && state.atoms.has(name)) {
|
|
|
|
|
loweringIssue(state, declaration, "ambiguous-conformance-source", name + " names both an atom and an interface");
|
|
|
|
|
continue;
|
2026-09-03 15:46:52 -07:00
|
|
|
}
|
2026-09-23 00:01:03 -07:00
|
|
|
if (state.interfaces.has(name)) {
|
|
|
|
|
const rule = atGenericSource(declaration, () => lowerConformanceRule(state, declaration));
|
|
|
|
|
if (rule) rules.set(rule, declaration);
|
|
|
|
|
} else {
|
|
|
|
|
if (declaration.typeParameters() || declaration.conformanceSourceArguments())
|
|
|
|
|
loweringIssue(state, declaration, "invalid-conformance-source", "Only interface rules take source parameters");
|
|
|
|
|
direct.push(declaration);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const origins = new Map<WorkspaceRevision["conformances"][number], ConformanceDeclContext>();
|
|
|
|
|
const conformances = direct.flatMap((declaration) => {
|
|
|
|
|
const atom = state.atoms.get(identifier(declaration.identifier(0)));
|
|
|
|
|
const result = atGenericSource(declaration, () =>
|
|
|
|
|
withConformanceTypes(state, atom, [], new Map(), () =>
|
|
|
|
|
lowerConformanceBody(state, declaration, privateAttachments.get(declaration) ?? []),
|
|
|
|
|
),
|
2026-09-16 00:12:39 -07:00
|
|
|
);
|
2026-09-23 00:01:03 -07:00
|
|
|
if (result) origins.set(result, declaration);
|
|
|
|
|
return result ? [result] : [];
|
2026-09-03 15:46:52 -07:00
|
|
|
});
|
2026-09-23 00:01:03 -07:00
|
|
|
if (!state.diagnostics.length) {
|
|
|
|
|
const applications = expandConformanceRules(
|
|
|
|
|
[...rules.keys()],
|
|
|
|
|
conformances,
|
|
|
|
|
state.types.definitions,
|
|
|
|
|
(atom, bindings) => ({
|
|
|
|
|
...state.types.environment(),
|
|
|
|
|
self: atom,
|
|
|
|
|
arguments: bindings,
|
|
|
|
|
applyInterface: (id, args) =>
|
|
|
|
|
withConformanceTypes(state, atom, [], bindings, () => state.types.apply(id, args).revisionId),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
for (const application of applications) {
|
|
|
|
|
const declaration = rules.get(application.rule)!;
|
|
|
|
|
const result = atGenericSource(declaration, () =>
|
|
|
|
|
withConformanceTypes(state, application.atomId, application.rule.parameters, application.bindings, () =>
|
2026-09-25 10:53:19 -07:00
|
|
|
lowerConformanceBody(state, declaration, [], application.atomId, application.id),
|
2026-09-23 00:01:03 -07:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
if (result) {
|
|
|
|
|
const generated = {
|
|
|
|
|
...result,
|
|
|
|
|
id: application.id,
|
|
|
|
|
ruleOrigin: {
|
|
|
|
|
ruleId: application.rule.id,
|
|
|
|
|
sourceInterfaceRevisionId: application.sourceId,
|
|
|
|
|
line: declaration.start?.line ?? 0,
|
|
|
|
|
column: declaration.start?.column ?? 0,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
conformances.push(generated);
|
|
|
|
|
origins.set(generated, declaration);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const candidates = new Map<string, WorkspaceRevision["conformances"][number]>();
|
|
|
|
|
for (const conformance of conformances) {
|
|
|
|
|
const key = JSON.stringify([conformance.atomId, conformance.interfaceRevisionId]);
|
|
|
|
|
const previous = candidates.get(key);
|
|
|
|
|
if (previous) {
|
|
|
|
|
const describe = (c: typeof conformance) =>
|
|
|
|
|
c.ruleOrigin
|
|
|
|
|
? "rule " + c.ruleOrigin.ruleId + " from " + c.ruleOrigin.sourceInterfaceRevisionId
|
|
|
|
|
: "conformance " + (c.id ?? "(unnamed)");
|
|
|
|
|
const message =
|
|
|
|
|
"Duplicate conformance for " +
|
|
|
|
|
conformance.atomId +
|
|
|
|
|
" as " +
|
|
|
|
|
conformance.interfaceRevisionId +
|
|
|
|
|
": " +
|
|
|
|
|
describe(previous) +
|
|
|
|
|
" conflicts with " +
|
|
|
|
|
describe(conformance);
|
|
|
|
|
for (const candidate of [previous, conformance])
|
|
|
|
|
loweringIssue(state, origins.get(candidate)!, "duplicate-conformance", message);
|
|
|
|
|
} else candidates.set(key, conformance);
|
|
|
|
|
}
|
2026-09-03 15:46:52 -07:00
|
|
|
|
|
|
|
|
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");
|
2026-09-15 15:23:24 -07:00
|
|
|
const packageSymbol = requireSymbol(state, state.packages, packageName, constructor, "package");
|
2026-09-03 15:46:52 -07:00
|
|
|
const exportSymbol = packageSymbol
|
2026-09-15 15:23:24 -07:00
|
|
|
? requireSymbol(state, packageSymbol.exports, exportName, constructor, `export on ${packageName}`)
|
2026-09-03 15:46:52 -07:00
|
|
|
: undefined;
|
|
|
|
|
return atomId && packageSymbol && exportSymbol
|
|
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
atomId,
|
|
|
|
|
packageRevisionId: packageSymbol.revisionId,
|
|
|
|
|
exportId: exportSymbol.exportId,
|
|
|
|
|
dependencies: lowerBoundDependencies(
|
|
|
|
|
state,
|
|
|
|
|
constructor.dependencyBindingBlock() ?? undefined,
|
|
|
|
|
exportSymbol,
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
: [];
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-21 11:23:26 -07:00
|
|
|
const presentations: NonNullable<WorkspaceRevision["presentations"]> = items.flatMap((item) => {
|
|
|
|
|
const presentation = item.presentationDecl();
|
|
|
|
|
if (!presentation) return [];
|
|
|
|
|
const packageName = identifier(presentation.identifier(1));
|
|
|
|
|
const componentName = identifier(presentation.identifier(2));
|
|
|
|
|
const pkg = requireSymbol(state, state.packages, packageName, presentation, "package");
|
2026-09-22 13:16:23 -07:00
|
|
|
const component = pkg?.definition?.modules?.find((entry) => entry.displayName === componentName);
|
2026-09-21 11:23:26 -07:00
|
|
|
if (!component) {
|
|
|
|
|
loweringIssue(state, presentation, "unknown-component", `Unknown component ${packageName}.${componentName}`);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
id: stringValue(presentation.stringLiteral()),
|
|
|
|
|
displayName: identifier(presentation.identifier(0)),
|
|
|
|
|
subject: new TypeSubstitution(state.types.environment()).application(
|
|
|
|
|
state.types.interface(presentation.interfaceType()),
|
|
|
|
|
),
|
|
|
|
|
packageRevisionId: pkg!.revisionId,
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-03 15:46:52 -07:00
|
|
|
return {
|
2026-09-05 12:33:52 -07:00
|
|
|
imports,
|
|
|
|
|
workspace: {
|
|
|
|
|
id: capabilityId.workspaceRevision(stringValue(context.stringLiteral(1))),
|
|
|
|
|
workspaceId: capabilityId.workspace(stringValue(context.stringLiteral(0))),
|
|
|
|
|
parentRevisionIds: [],
|
|
|
|
|
sourceRootCommit: stringValue(context.stringLiteral(2)),
|
|
|
|
|
atoms,
|
|
|
|
|
sharedAttachments,
|
2026-09-16 00:12:39 -07:00
|
|
|
interfaceImports: uniqueExactRevisions([
|
|
|
|
|
...interfaceImports.filter((entry) => !entry.template),
|
|
|
|
|
...state.types.applications.values(),
|
|
|
|
|
]),
|
|
|
|
|
packageImports: packageImports.map(
|
|
|
|
|
(revision) =>
|
|
|
|
|
[...state.packages.values()].find((entry) => entry.revisionId === revision.revisionId)?.definition ??
|
|
|
|
|
revision,
|
|
|
|
|
),
|
2026-09-05 12:33:52 -07:00
|
|
|
conformances,
|
|
|
|
|
constructors,
|
2026-09-21 11:23:26 -07:00
|
|
|
...(presentations.length ? { presentations } : {}),
|
2026-09-05 12:33:52 -07:00
|
|
|
},
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-08 14:27:24 -07:00
|
|
|
export const parseDocument = (
|
2026-09-03 15:46:52 -07:00
|
|
|
source: string,
|
2026-09-05 12:33:52 -07:00
|
|
|
fileName: string,
|
2026-09-08 14:27:24 -07:00
|
|
|
): { tree: DocumentContext; tokens: CommonTokenStream; diagnostics: CapabilitySourceDiagnostic[] } => {
|
2026-09-03 15:46:52 -07:00
|
|
|
const diagnostics: CapabilitySourceDiagnostic[] = [];
|
|
|
|
|
const listener = new SyntaxErrorListener(fileName, diagnostics);
|
|
|
|
|
const lexer = new QuixosCapabilityLexer(CharStream.fromString(source));
|
|
|
|
|
lexer.removeErrorListeners();
|
|
|
|
|
lexer.addErrorListener(listener);
|
2026-09-08 14:27:24 -07:00
|
|
|
const tokens = new CommonTokenStream(lexer);
|
|
|
|
|
const parser = new QuixosCapabilityParser(tokens);
|
2026-09-03 15:46:52 -07:00
|
|
|
parser.removeErrorListeners();
|
|
|
|
|
parser.addErrorListener(listener);
|
|
|
|
|
const tree = parser.document();
|
2026-09-08 14:27:24 -07:00
|
|
|
tokens.fill();
|
|
|
|
|
return { tree, tokens, diagnostics };
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
|
2026-09-16 00:12:39 -07:00
|
|
|
const newLoweringState = (fileName: string, diagnostics: CapabilitySourceDiagnostic[]): LoweringState => {
|
|
|
|
|
const atoms = new Map<string, AtomId>();
|
|
|
|
|
return {
|
|
|
|
|
fileName,
|
|
|
|
|
diagnostics,
|
|
|
|
|
atoms,
|
|
|
|
|
types: new GenericSourceTypes(atoms),
|
|
|
|
|
interfaces: new Map(),
|
|
|
|
|
packages: new Map(),
|
|
|
|
|
attachments: new Map(),
|
|
|
|
|
};
|
|
|
|
|
};
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
const validationDiagnostics = (
|
|
|
|
|
fileName: string,
|
|
|
|
|
issues: ReturnType<typeof compileWorkspaceRevision> extends infer _Result
|
|
|
|
|
? Array<{ code: string; message: string; path: string }>
|
|
|
|
|
: never,
|
2026-09-15 15:23:24 -07:00
|
|
|
): CapabilitySourceDiagnostic[] =>
|
|
|
|
|
issues.map((entry) => ({
|
|
|
|
|
phase: "validation",
|
|
|
|
|
code: entry.code,
|
|
|
|
|
message: entry.message,
|
|
|
|
|
fileName,
|
|
|
|
|
line: 0,
|
|
|
|
|
column: 0,
|
|
|
|
|
path: entry.path,
|
|
|
|
|
}));
|
2026-09-05 12:33:52 -07:00
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
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;
|
|
|
|
|
});
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
const externalInterfacesFrom = (
|
|
|
|
|
state: LoweringState,
|
|
|
|
|
contexts: readonly ExternalInterfaceDeclContext[],
|
2026-09-15 15:23:24 -07:00
|
|
|
): 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;
|
|
|
|
|
});
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
const resourcePreambleParts = (contexts: readonly ResourcePreambleContext[]) => ({
|
|
|
|
|
imports: contexts.flatMap((context) => context.resourceImportDecl() ?? []),
|
|
|
|
|
atoms: contexts.flatMap((context) => context.externalAtomDecl() ?? []),
|
|
|
|
|
interfaces: contexts.flatMap((context) => context.externalInterfaceDecl() ?? []),
|
2026-09-16 00:12:39 -07:00
|
|
|
aliases: contexts.flatMap((context) => context.typeAliasDecl() ?? []),
|
2026-09-05 12:33:52 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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] : []),
|
|
|
|
|
]);
|
2026-09-15 15:23:24 -07:00
|
|
|
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] : []),
|
2026-09-16 00:12:39 -07:00
|
|
|
...(resource.specializations ?? []),
|
2026-09-15 15:23:24 -07:00
|
|
|
...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: [],
|
|
|
|
|
}),
|
|
|
|
|
),
|
2026-09-16 00:12:39 -07:00
|
|
|
])
|
|
|
|
|
.filter((entry) => !entry.template)
|
|
|
|
|
.map((entry) => ({ ...entry, argumentRequirements: [] })),
|
2026-09-15 15:23:24 -07:00
|
|
|
packageImports: uniqueExactRevisions([
|
|
|
|
|
...(environment.packageClosure ?? []),
|
|
|
|
|
...[...(environment.packages?.values() ?? [])],
|
|
|
|
|
...(resource.kind === "package" ? [resource.revision] : []),
|
|
|
|
|
]),
|
|
|
|
|
conformances: [],
|
|
|
|
|
constructors: [],
|
|
|
|
|
};
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
|
2026-09-16 00:12:39 -07:00
|
|
|
const compileCapabilityResourceSourceInternal = (
|
2026-09-05 12:33:52 -07:00
|
|
|
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);
|
2026-09-03 15:46:52 -07:00
|
|
|
if (diagnostics.length > 0) {
|
|
|
|
|
return { ok: false, diagnostics };
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-05 12:33:52 -07:00
|
|
|
const interfaceContext = tree.interfaceResourceDecl();
|
|
|
|
|
const packageContext = tree.packageResourceDecl();
|
|
|
|
|
if (!interfaceContext && !packageContext) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
2026-09-15 15:23:24 -07:00
|
|
|
diagnostics: [
|
|
|
|
|
{
|
|
|
|
|
phase: "lowering",
|
|
|
|
|
code: "expected-resource",
|
|
|
|
|
message: "Expected a standalone interface or package resource document",
|
|
|
|
|
fileName,
|
|
|
|
|
line: 1,
|
|
|
|
|
column: 0,
|
|
|
|
|
},
|
|
|
|
|
],
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state = newLoweringState(fileName, diagnostics);
|
2026-09-15 15:23:24 -07:00
|
|
|
const preamble = resourcePreambleParts((interfaceContext ?? packageContext)!.resourcePreamble());
|
2026-09-05 12:33:52 -07:00
|
|
|
const externalAtoms = externalAtomsFrom(state, preamble.atoms);
|
|
|
|
|
const imports = registerImports(state, preamble.imports, environment);
|
|
|
|
|
const externalInterfaces = externalInterfacesFrom(state, preamble.interfaces);
|
2026-09-16 00:12:39 -07:00
|
|
|
state.types.declareAliases(preamble.aliases);
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
let resource: CapabilityResource;
|
|
|
|
|
if (interfaceContext) {
|
|
|
|
|
const alias = identifier(interfaceContext.identifier());
|
2026-09-16 00:12:39 -07:00
|
|
|
state.types.register(alias, {
|
|
|
|
|
interfaceId: capabilityId.interface(stringValue(interfaceContext.stringLiteral(0))),
|
|
|
|
|
revisionId: capabilityId.interfaceRevision(stringValue(interfaceContext.stringLiteral(1))),
|
|
|
|
|
displayName: alias,
|
|
|
|
|
source: options.source,
|
|
|
|
|
members: [],
|
|
|
|
|
});
|
2026-09-05 12:33:52 -07:00
|
|
|
declareSymbol(
|
|
|
|
|
state,
|
|
|
|
|
state.interfaces,
|
|
|
|
|
alias,
|
|
|
|
|
{
|
2026-09-15 15:23:24 -07:00
|
|
|
revisionId: capabilityId.interfaceRevision(stringValue(interfaceContext.stringLiteral(1))),
|
2026-09-05 12:33:52 -07:00
|
|
|
contractAvailable: true,
|
|
|
|
|
members: new Map(),
|
|
|
|
|
},
|
|
|
|
|
interfaceContext,
|
|
|
|
|
"interface",
|
|
|
|
|
);
|
|
|
|
|
resource = {
|
|
|
|
|
kind: "interface",
|
|
|
|
|
imports,
|
|
|
|
|
externalAtoms,
|
|
|
|
|
externalInterfaces,
|
2026-09-16 00:12:39 -07:00
|
|
|
revision:
|
|
|
|
|
interfaceContext.typeParameters() || hasSelfIdentifier(interfaceContext, state)
|
|
|
|
|
? atGenericSource(interfaceContext, () => lowerInterfaceTemplate(state, interfaceContext, options.source))
|
|
|
|
|
: lowerInterface(state, interfaceContext, options.source),
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
const context = packageContext!;
|
|
|
|
|
const alias = identifier(context.identifier());
|
|
|
|
|
declareSymbol(
|
|
|
|
|
state,
|
|
|
|
|
state.packages,
|
|
|
|
|
alias,
|
|
|
|
|
{
|
2026-09-15 15:23:24 -07:00
|
|
|
revisionId: capabilityId.packageRevision(stringValue(context.stringLiteral(1))),
|
2026-09-05 12:33:52 -07:00
|
|
|
exports: new Map(),
|
|
|
|
|
},
|
|
|
|
|
context,
|
|
|
|
|
"package",
|
|
|
|
|
);
|
|
|
|
|
resource = {
|
|
|
|
|
kind: "package",
|
|
|
|
|
imports,
|
|
|
|
|
externalAtoms,
|
|
|
|
|
externalInterfaces,
|
|
|
|
|
revision: lowerPackage(state, context, options.source),
|
|
|
|
|
};
|
2026-09-22 13:16:23 -07:00
|
|
|
if (resource.revision.modules?.length) {
|
2026-09-22 16:19:05 -07:00
|
|
|
resource.revision.clientPackages = Object.fromEntries(
|
|
|
|
|
imports.flatMap((entry) => {
|
|
|
|
|
if (entry.kind !== "package") return [];
|
|
|
|
|
const imported = options.environment?.packages?.get(entry.binding);
|
|
|
|
|
return imported ? [[entry.binding, imported.revisionId]] : [];
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-09-21 15:24:05 -07:00
|
|
|
resource.revision.clientInterfaces = Object.fromEntries(
|
|
|
|
|
imports.flatMap((entry) => {
|
|
|
|
|
if (entry.kind !== "interface") return [];
|
|
|
|
|
const contract = options.environment?.interfaces?.get(entry.binding);
|
|
|
|
|
return contract && !contract.template ? [[entry.binding, contract.revisionId]] : [];
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-09-05 12:33:52 -07:00
|
|
|
}
|
|
|
|
|
|
2026-09-16 00:12:39 -07:00
|
|
|
if (resource.kind === "interface") {
|
|
|
|
|
state.types.register(identifier(interfaceContext!.identifier()), resource.revision);
|
|
|
|
|
atGenericSource(interfaceContext!, () => state.types.validateTemplate(resource.revision));
|
|
|
|
|
} else {
|
|
|
|
|
const names = new Set(resource.revision.exports.map((entry) => entry.displayName));
|
|
|
|
|
const ids = new Set(resource.revision.exports.map((entry) => entry.id));
|
|
|
|
|
for (const entry of resource.revision.genericExports ?? []) {
|
|
|
|
|
if (names.has(entry.displayName) || ids.has(entry.id))
|
|
|
|
|
throw new GenericTypeError("duplicate-package-export", entry.displayName, "Duplicate generic export");
|
|
|
|
|
names.add(entry.displayName);
|
|
|
|
|
ids.add(entry.id);
|
|
|
|
|
if (
|
|
|
|
|
entry.kind === "operation" &&
|
|
|
|
|
(entry.mode === "watch-start" || entry.mode === "subscribe") !== Boolean(entry.eventType)
|
|
|
|
|
)
|
|
|
|
|
throw new GenericTypeError(
|
|
|
|
|
"invalid-operation",
|
|
|
|
|
entry.displayName,
|
|
|
|
|
"Only watch-start/subscribe operations must declare an event type",
|
|
|
|
|
);
|
|
|
|
|
const portIds = new Set<string>(),
|
|
|
|
|
portNames = new Set<string>();
|
|
|
|
|
const types: ValueTypeExpression[] = [
|
|
|
|
|
entry.inputType,
|
|
|
|
|
entry.outputType,
|
|
|
|
|
...(entry.eventType ? [entry.eventType] : []),
|
|
|
|
|
];
|
|
|
|
|
const requires = entry.parameters.flatMap((parameter) =>
|
|
|
|
|
parameter.kind === "object" ? parameter.implements : [],
|
|
|
|
|
);
|
|
|
|
|
for (const port of entry.dependencyPorts) {
|
|
|
|
|
if (portIds.has(port.id) || portNames.has(port.displayName))
|
|
|
|
|
throw new GenericTypeError("duplicate-dependency-port", entry.displayName, "Duplicate port");
|
|
|
|
|
portIds.add(port.id);
|
|
|
|
|
portNames.add(port.displayName);
|
|
|
|
|
const requirement = port.requirement;
|
|
|
|
|
if (requirement.kind === "state") {
|
2026-09-19 22:20:55 -07:00
|
|
|
if (requirement.primitives.some((p) => !["read", "write", "edit", "watch-start", "watch-stop"].includes(p)))
|
2026-09-16 00:12:39 -07:00
|
|
|
throw new GenericTypeError("invalid-port-primitive", port.displayName, "Invalid state primitive");
|
|
|
|
|
types.push(requirement.valueType);
|
|
|
|
|
}
|
|
|
|
|
if (requirement.kind === "edge") types.push({ kind: "object-ref", expectation: requirement.target });
|
2026-09-19 22:09:55 -07:00
|
|
|
if (requirement.kind === "constructor" || requirement.kind === "creation")
|
2026-09-16 00:12:39 -07:00
|
|
|
types.push(requirement.inputType, { kind: "object-ref", expectation: requirement.target });
|
|
|
|
|
if (requirement.kind === "interface") requires.push(requirement.application);
|
|
|
|
|
}
|
|
|
|
|
if (entry.receiverRequirement.kind === "interfaces") requires.push(...entry.receiverRequirement.interfaces);
|
|
|
|
|
state.types.validateTemplate({
|
|
|
|
|
interfaceId: capabilityId.interface(entry.id),
|
|
|
|
|
revisionId: capabilityId.interfaceRevision(entry.id),
|
|
|
|
|
displayName: entry.displayName,
|
|
|
|
|
source: resource.revision.source,
|
|
|
|
|
members: [],
|
|
|
|
|
template: {
|
|
|
|
|
parameters: entry.parameters,
|
|
|
|
|
aliases: entry.aliases,
|
|
|
|
|
usesSelf: false,
|
|
|
|
|
requires,
|
|
|
|
|
members: types.map((type, index) => ({
|
|
|
|
|
kind: "value",
|
|
|
|
|
id: capabilityId.member(String(index)),
|
|
|
|
|
displayName: String(index),
|
|
|
|
|
valueType: type,
|
|
|
|
|
operations: [],
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
resource.specializations = [...state.types.applications.values()];
|
|
|
|
|
|
2026-09-03 15:46:52 -07:00
|
|
|
if (diagnostics.length > 0) {
|
|
|
|
|
return { ok: false, diagnostics };
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
const compiled = compileWorkspaceRevision(resourceValidationWorkspace(resource, environment));
|
2026-09-03 15:46:52 -07:00
|
|
|
if (!compiled.ok) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
2026-09-05 12:33:52 -07:00
|
|
|
diagnostics: validationDiagnostics(fileName, compiled.issues),
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
|
|
|
|
}
|
2026-09-05 12:33:52 -07:00
|
|
|
return { ok: true, resource, diagnostics: [] };
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-16 00:12:39 -07:00
|
|
|
const compileCapabilitySourceInternal = (
|
2026-09-05 12:33:52 -07:00
|
|
|
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,
|
2026-09-15 15:23:24 -07:00
|
|
|
diagnostics: [
|
|
|
|
|
{
|
|
|
|
|
phase: "lowering",
|
|
|
|
|
code: "expected-workspace",
|
|
|
|
|
message: "Expected a workspace document",
|
|
|
|
|
fileName,
|
|
|
|
|
line: 1,
|
|
|
|
|
column: 0,
|
|
|
|
|
},
|
|
|
|
|
],
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state = newLoweringState(fileName, diagnostics);
|
2026-09-08 14:27:24 -07:00
|
|
|
for (const item of workspaceContext.workspaceItem()) {
|
2026-09-15 15:23:24 -07:00
|
|
|
if (item.sourceImportDecl())
|
|
|
|
|
loweringIssue(state, item, "unresolved-source-import", "Local imports require the workspace repository compiler");
|
2026-09-08 14:27:24 -07:00
|
|
|
}
|
|
|
|
|
if (diagnostics.length) return { ok: false, diagnostics };
|
2026-09-05 12:33:52 -07:00
|
|
|
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: [],
|
|
|
|
|
};
|
2026-09-03 15:46:52 -07:00
|
|
|
};
|
2026-09-16 00:12:39 -07:00
|
|
|
|
|
|
|
|
const genericDiagnostic = (error: GenericTypeError, fileName: string): CapabilitySourceDiagnostic => ({
|
|
|
|
|
phase: "lowering",
|
|
|
|
|
code: error.code,
|
|
|
|
|
message: error.message,
|
|
|
|
|
fileName,
|
|
|
|
|
line: genericLocations.get(error)?.line ?? 0,
|
|
|
|
|
column: genericLocations.get(error)?.column ?? 0,
|
|
|
|
|
path: error.path,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const compileCapabilityResourceSource = (
|
|
|
|
|
...args: Parameters<typeof compileCapabilityResourceSourceInternal>
|
|
|
|
|
): CapabilityResourceCompileResult => {
|
|
|
|
|
try {
|
|
|
|
|
return compileCapabilityResourceSourceInternal(...args);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (!(error instanceof GenericTypeError)) throw error;
|
|
|
|
|
return { ok: false, diagnostics: [genericDiagnostic(error, args[1].fileName ?? "<memory>")] };
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const compileCapabilitySource = (
|
|
|
|
|
...args: Parameters<typeof compileCapabilitySourceInternal>
|
|
|
|
|
): CapabilitySourceCompileResult => {
|
|
|
|
|
try {
|
|
|
|
|
return compileCapabilitySourceInternal(...args);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (!(error instanceof GenericTypeError)) throw error;
|
|
|
|
|
return { ok: false, diagnostics: [genericDiagnostic(error, args[1] ?? "<memory>")] };
|
|
|
|
|
}
|
|
|
|
|
};
|