Add checked private storage to reusable conformance rules
Scope attachments and projections by stable rule-application identity; keep lexical names private to each application. Lower ordinary initialization mappings and concrete captured creation targets through existing checking. Preserve stateless forwarding, conflict detection, and the materialization fence. Add tests for multiple same-atom specializations, name isolation, generic edges, factory seeds, and storage evolution. Full protocol Nix build and tests pass.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
BaseErrorListener,
|
||||
CharStream,
|
||||
@@ -1712,7 +1713,8 @@ const lowerPackage = (
|
||||
const lowerState = (state: LoweringState, context: StateDeclContext): StateSlotDefinition => {
|
||||
const atomName = identifier(context.identifier(1));
|
||||
const atomId =
|
||||
requireSymbol(state, state.atoms, atomName, context, "atom") ?? capabilityId.atom(`unresolved:${atomName}`);
|
||||
(atomName === "Self" ? state.types.self : requireSymbol(state, state.atoms, atomName, context, "atom")) ??
|
||||
capabilityId.atom(`unresolved:${atomName}`);
|
||||
const policy = context.storagePolicy();
|
||||
const defaultContext = context.jsonLiteral();
|
||||
let defaultValue: unknown;
|
||||
@@ -1932,7 +1934,18 @@ const lowerBoundDependencies = (
|
||||
: [];
|
||||
}
|
||||
const atomName = identifier(entry.identifier(1));
|
||||
const atomId = requireSymbol(state, state.atoms, atomName, entry, "atom");
|
||||
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");
|
||||
if (entry.CREATION())
|
||||
return atomId
|
||||
? [
|
||||
@@ -2031,19 +2044,90 @@ const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclCont
|
||||
const targetDefinition = state.types.definitions.get(target.definitionId)!;
|
||||
const applications = [source, target];
|
||||
const members = targetDefinition.template?.members ?? targetDefinition.members;
|
||||
const privateStorage = new Map<string, "state" | "edge">();
|
||||
const privateIds = new Set<string>();
|
||||
for (const item of context.conformanceItem()) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (
|
||||
item.attachmentDecl() ||
|
||||
item.initializationDecl() ||
|
||||
item.relationshipMaterializationDecl() ||
|
||||
item.stateFieldBindingDecl()
|
||||
) {
|
||||
loweringIssue(
|
||||
state,
|
||||
item,
|
||||
"unsupported-rule-storage",
|
||||
"Rules support operation and module bindings, not private storage, initialization or materialization",
|
||||
);
|
||||
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");
|
||||
continue;
|
||||
}
|
||||
const module = item.moduleBindingDecl();
|
||||
@@ -2095,6 +2179,28 @@ const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclCont
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (provider.STATE() || provider.EDGE()) {
|
||||
const name = identifier(provider.identifier(0));
|
||||
const expected = provider.STATE() ? "state" : "edge";
|
||||
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");
|
||||
}
|
||||
} else {
|
||||
loweringIssue(
|
||||
state,
|
||||
@@ -2123,8 +2229,13 @@ const lowerConformanceBody = (
|
||||
context: ConformanceDeclContext,
|
||||
privateAttachments: PersistentAttachment[],
|
||||
implementingAtom?: AtomId,
|
||||
ruleApplicationId?: string,
|
||||
): WorkspaceRevision["conformances"][number] | undefined => {
|
||||
const state = { ...parent, interfaces: new Map(parent.interfaces) };
|
||||
const state = {
|
||||
...parent,
|
||||
interfaces: new Map(parent.interfaces),
|
||||
attachments: ruleApplicationId ? new Map(parent.attachments) : parent.attachments,
|
||||
};
|
||||
const atomName = identifier(context.identifier(0));
|
||||
const interfaceName = identifier(context.identifier(1));
|
||||
const atomId = implementingAtom ?? requireSymbol(state, state.atoms, atomName, context, "atom");
|
||||
@@ -2132,6 +2243,36 @@ const lowerConformanceBody = (
|
||||
if (!atomId || !interfaceSymbol) {
|
||||
return undefined;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
const originalSymbol = interfaceSymbol;
|
||||
if (context.typeArguments() || interfaceSymbol.definition?.template) {
|
||||
const previousSelf = state.types.self;
|
||||
@@ -2654,7 +2795,7 @@ const lowerWorkspace = (
|
||||
const declaration = rules.get(application.rule)!;
|
||||
const result = atGenericSource(declaration, () =>
|
||||
withConformanceTypes(state, application.atomId, application.rule.parameters, application.bindings, () =>
|
||||
lowerConformanceBody(state, declaration, [], application.atomId),
|
||||
lowerConformanceBody(state, declaration, [], application.atomId, application.id),
|
||||
),
|
||||
);
|
||||
if (result) {
|
||||
|
||||
Reference in New Issue
Block a user