Compile and dispatch authoritative acceptance hooks with target-specific package roots

This commit is contained in:
Timothy J. Aveni
2026-09-20 22:05:59 -07:00
parent 1e69b0e662
commit 266d2d98d0
16 changed files with 3112 additions and 2701 deletions
+12 -3
View File
@@ -169,12 +169,16 @@ queryClause
packageOperationExport packageOperationExport
: OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType : OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? SEMI MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? acceptanceClause? SEMI
; ;
packageFunctionExport packageFunctionExport
: FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType : FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
executionClause? dependencyBlock? SEMI executionClause? dependencyBlock? acceptanceClause? SEMI
;
acceptanceClause
: ACCEPT USING identifier
; ;
executionClause executionClause
@@ -332,7 +336,11 @@ operationProvider
: STATE identifier DOT statePrimitive : STATE identifier DOT statePrimitive
| EDGE identifier DOT identifier DOT edgePrimitive | EDGE identifier DOT identifier DOT edgePrimitive
| INTERFACE identifier typeArguments? DOT memberOperationRef | INTERFACE identifier typeArguments? DOT memberOperationRef
| PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock? | PACKAGE identifier DOT identifier typeArguments? dependencyBindingBlock? acceptanceBinding?
;
acceptanceBinding
: ACCEPT dependencyBindingBlock
; ;
statePrimitive statePrimitive
@@ -528,6 +536,7 @@ EDGE: 'edge';
PROJECTION: 'projection'; PROJECTION: 'projection';
WITH: 'with'; WITH: 'with';
USING: 'using'; USING: 'using';
ACCEPT: 'accept';
VIA: 'via'; VIA: 'via';
MATERIALIZE: 'materialize'; MATERIALIZE: 'materialize';
IF: 'if'; IF: 'if';
File diff suppressed because one or more lines are too long
@@ -58,92 +58,93 @@ EDGE=57
PROJECTION=58 PROJECTION=58
WITH=59 WITH=59
USING=60 USING=60
VIA=61 ACCEPT=61
MATERIALIZE=62 VIA=62
IF=63 MATERIALIZE=63
ABSENT=64 IF=64
ON=65 ABSENT=65
POLICY=66 ON=66
DEFAULT=67 POLICY=67
SOURCE=68 DEFAULT=68
REPOSITORY=69 SOURCE=69
COMMIT=70 REPOSITORY=70
REVISION=71 COMMIT=71
SEMANTIC_MAJOR=72 REVISION=72
ON_DELETE=73 SEMANTIC_MAJOR=73
RETAIN_OTHER=74 ON_DELETE=74
KEYED=75 RETAIN_OTHER=75
PUBLIC_TRAVERSAL=76 KEYED=76
ID=77 PUBLIC_TRAVERSAL=77
DOC=78 ID=78
MODE=79 DOC=79
EMITS=80 MODE=80
RECEIVER=81 EMITS=81
REQUIRES=82 RECEIVER=82
ANY=83 REQUIRES=83
GET=84 ANY=84
SET=85 GET=85
WATCH=86 SET=86
START=87 WATCH=87
STOP=88 START=88
READ=89 STOP=89
WRITE=90 READ=90
RESOLVE=91 WRITE=91
CONNECT=92 RESOLVE=92
DISCONNECT=93 CONNECT=93
CALL=94 DISCONNECT=94
WATCH_START=95 CALL=95
WATCH_STOP=96 WATCH_START=96
SUBSCRIBE=97 WATCH_STOP=97
UNSUBSCRIBE=98 SUBSCRIBE=98
REGISTER=99 UNSUBSCRIBE=99
CRDT=100 REGISTER=100
OPTIONAL_ONE=101 CRDT=101
EXACTLY_ONE=102 OPTIONAL_ONE=102
MANY_UNIQUE=103 EXACTLY_ONE=103
MANY=104 MANY_UNIQUE=104
ORDERED=105 MANY=105
UNIT=106 ORDERED=106
WATCH_HANDLE=107 UNIT=107
MESSAGE=108 WATCH_HANDLE=108
ATOM_REF=109 MESSAGE=109
INTERFACE_REF=110 ATOM_REF=110
OPTIONAL=111 INTERFACE_REF=111
LIST=112 OPTIONAL=112
RECORD=113 LIST=113
BOOL=114 RECORD=114
BYTES=115 BOOL=115
DOUBLE=116 BYTES=116
INT32=117 DOUBLE=117
INT64=118 INT32=118
STRING=119 INT64=119
UINT32=120 STRING=120
UINT64=121 UINT32=121
TRUE=122 UINT64=122
FALSE=123 TRUE=123
NULL=124 FALSE=124
ARROW=125 NULL=125
COLON=126 ARROW=126
SEMI=127 COLON=127
COMMA=128 SEMI=128
DOT=129 COMMA=129
LBRACE=130 DOT=130
RBRACE=131 LBRACE=131
LBRACK=132 RBRACE=132
RBRACK=133 LBRACK=133
LPAREN=134 RBRACK=134
RPAREN=135 LPAREN=135
LT=136 RPAREN=136
GT=137 LT=137
AMP=138 GT=138
EQUAL=139 AMP=139
INTEGER=140 EQUAL=140
JSON_NUMBER=141 INTEGER=141
IDENTIFIER=142 JSON_NUMBER=142
STRING_LITERAL=143 IDENTIFIER=143
LINE_COMMENT=144 STRING_LITERAL=144
BLOCK_COMMENT=145 LINE_COMMENT=145
WS=146 BLOCK_COMMENT=146
WS=147
'edit'=1 'edit'=1
'creation'=2 'creation'=2
'initialize'=3 'initialize'=3
@@ -204,82 +205,83 @@ WS=146
'projection'=58 'projection'=58
'with'=59 'with'=59
'using'=60 'using'=60
'via'=61 'accept'=61
'materialize'=62 'via'=62
'if'=63 'materialize'=63
'absent'=64 'if'=64
'on'=65 'absent'=65
'policy'=66 'on'=66
'default'=67 'policy'=67
'source'=68 'default'=68
'repository'=69 'source'=69
'commit'=70 'repository'=70
'revision'=71 'commit'=71
'semantic-major'=72 'revision'=72
'on-delete'=73 'semantic-major'=73
'retain-other'=74 'on-delete'=74
'keyed'=75 'retain-other'=75
'public-traversal'=76 'keyed'=76
'id'=77 'public-traversal'=77
'doc'=78 'id'=78
'mode'=79 'doc'=79
'emits'=80 'mode'=80
'receiver'=81 'emits'=81
'requires'=82 'receiver'=82
'any'=83 'requires'=83
'get'=84 'any'=84
'set'=85 'get'=85
'watch'=86 'set'=86
'start'=87 'watch'=87
'stop'=88 'start'=88
'read'=89 'stop'=89
'write'=90 'read'=90
'resolve'=91 'write'=91
'connect'=92 'resolve'=92
'disconnect'=93 'connect'=93
'call'=94 'disconnect'=94
'watch-start'=95 'call'=95
'watch-stop'=96 'watch-start'=96
'subscribe'=97 'watch-stop'=97
'unsubscribe'=98 'subscribe'=98
'register'=99 'unsubscribe'=99
'crdt'=100 'register'=100
'optional-one'=101 'crdt'=101
'exactly-one'=102 'optional-one'=102
'many-unique'=103 'exactly-one'=103
'many'=104 'many-unique'=104
'ordered'=105 'many'=105
'unit'=106 'ordered'=106
'watch-handle'=107 'unit'=107
'message'=108 'watch-handle'=108
'atom-ref'=109 'message'=109
'interface-ref'=110 'atom-ref'=110
'optional'=111 'interface-ref'=111
'list'=112 'optional'=112
'record'=113 'list'=113
'bool'=114 'record'=114
'bytes'=115 'bool'=115
'double'=116 'bytes'=116
'int32'=117 'double'=117
'int64'=118 'int32'=118
'string'=119 'int64'=119
'uint32'=120 'string'=120
'uint64'=121 'uint32'=121
'true'=122 'uint64'=122
'false'=123 'true'=123
'null'=124 'false'=124
'->'=125 'null'=125
':'=126 '->'=126
';'=127 ':'=127
','=128 ';'=128
'.'=129 ','=129
'{'=130 '.'=130
'}'=131 '{'=131
'['=132 '}'=132
']'=133 '['=133
'('=134 ']'=134
')'=135 '('=135
'<'=136 ')'=136
'>'=137 '<'=137
'&'=138 '>'=138
'='=139 '&'=139
'='=140
File diff suppressed because one or more lines are too long
@@ -58,92 +58,93 @@ EDGE=57
PROJECTION=58 PROJECTION=58
WITH=59 WITH=59
USING=60 USING=60
VIA=61 ACCEPT=61
MATERIALIZE=62 VIA=62
IF=63 MATERIALIZE=63
ABSENT=64 IF=64
ON=65 ABSENT=65
POLICY=66 ON=66
DEFAULT=67 POLICY=67
SOURCE=68 DEFAULT=68
REPOSITORY=69 SOURCE=69
COMMIT=70 REPOSITORY=70
REVISION=71 COMMIT=71
SEMANTIC_MAJOR=72 REVISION=72
ON_DELETE=73 SEMANTIC_MAJOR=73
RETAIN_OTHER=74 ON_DELETE=74
KEYED=75 RETAIN_OTHER=75
PUBLIC_TRAVERSAL=76 KEYED=76
ID=77 PUBLIC_TRAVERSAL=77
DOC=78 ID=78
MODE=79 DOC=79
EMITS=80 MODE=80
RECEIVER=81 EMITS=81
REQUIRES=82 RECEIVER=82
ANY=83 REQUIRES=83
GET=84 ANY=84
SET=85 GET=85
WATCH=86 SET=86
START=87 WATCH=87
STOP=88 START=88
READ=89 STOP=89
WRITE=90 READ=90
RESOLVE=91 WRITE=91
CONNECT=92 RESOLVE=92
DISCONNECT=93 CONNECT=93
CALL=94 DISCONNECT=94
WATCH_START=95 CALL=95
WATCH_STOP=96 WATCH_START=96
SUBSCRIBE=97 WATCH_STOP=97
UNSUBSCRIBE=98 SUBSCRIBE=98
REGISTER=99 UNSUBSCRIBE=99
CRDT=100 REGISTER=100
OPTIONAL_ONE=101 CRDT=101
EXACTLY_ONE=102 OPTIONAL_ONE=102
MANY_UNIQUE=103 EXACTLY_ONE=103
MANY=104 MANY_UNIQUE=104
ORDERED=105 MANY=105
UNIT=106 ORDERED=106
WATCH_HANDLE=107 UNIT=107
MESSAGE=108 WATCH_HANDLE=108
ATOM_REF=109 MESSAGE=109
INTERFACE_REF=110 ATOM_REF=110
OPTIONAL=111 INTERFACE_REF=111
LIST=112 OPTIONAL=112
RECORD=113 LIST=113
BOOL=114 RECORD=114
BYTES=115 BOOL=115
DOUBLE=116 BYTES=116
INT32=117 DOUBLE=117
INT64=118 INT32=118
STRING=119 INT64=119
UINT32=120 STRING=120
UINT64=121 UINT32=121
TRUE=122 UINT64=122
FALSE=123 TRUE=123
NULL=124 FALSE=124
ARROW=125 NULL=125
COLON=126 ARROW=126
SEMI=127 COLON=127
COMMA=128 SEMI=128
DOT=129 COMMA=129
LBRACE=130 DOT=130
RBRACE=131 LBRACE=131
LBRACK=132 RBRACE=132
RBRACK=133 LBRACK=133
LPAREN=134 RBRACK=134
RPAREN=135 LPAREN=135
LT=136 RPAREN=136
GT=137 LT=137
AMP=138 GT=138
EQUAL=139 AMP=139
INTEGER=140 EQUAL=140
JSON_NUMBER=141 INTEGER=141
IDENTIFIER=142 JSON_NUMBER=142
STRING_LITERAL=143 IDENTIFIER=143
LINE_COMMENT=144 STRING_LITERAL=144
BLOCK_COMMENT=145 LINE_COMMENT=145
WS=146 BLOCK_COMMENT=146
WS=147
'edit'=1 'edit'=1
'creation'=2 'creation'=2
'initialize'=3 'initialize'=3
@@ -204,82 +205,83 @@ WS=146
'projection'=58 'projection'=58
'with'=59 'with'=59
'using'=60 'using'=60
'via'=61 'accept'=61
'materialize'=62 'via'=62
'if'=63 'materialize'=63
'absent'=64 'if'=64
'on'=65 'absent'=65
'policy'=66 'on'=66
'default'=67 'policy'=67
'source'=68 'default'=68
'repository'=69 'source'=69
'commit'=70 'repository'=70
'revision'=71 'commit'=71
'semantic-major'=72 'revision'=72
'on-delete'=73 'semantic-major'=73
'retain-other'=74 'on-delete'=74
'keyed'=75 'retain-other'=75
'public-traversal'=76 'keyed'=76
'id'=77 'public-traversal'=77
'doc'=78 'id'=78
'mode'=79 'doc'=79
'emits'=80 'mode'=80
'receiver'=81 'emits'=81
'requires'=82 'receiver'=82
'any'=83 'requires'=83
'get'=84 'any'=84
'set'=85 'get'=85
'watch'=86 'set'=86
'start'=87 'watch'=87
'stop'=88 'start'=88
'read'=89 'stop'=89
'write'=90 'read'=90
'resolve'=91 'write'=91
'connect'=92 'resolve'=92
'disconnect'=93 'connect'=93
'call'=94 'disconnect'=94
'watch-start'=95 'call'=95
'watch-stop'=96 'watch-start'=96
'subscribe'=97 'watch-stop'=97
'unsubscribe'=98 'subscribe'=98
'register'=99 'unsubscribe'=99
'crdt'=100 'register'=100
'optional-one'=101 'crdt'=101
'exactly-one'=102 'optional-one'=102
'many-unique'=103 'exactly-one'=103
'many'=104 'many-unique'=104
'ordered'=105 'many'=105
'unit'=106 'ordered'=106
'watch-handle'=107 'unit'=107
'message'=108 'watch-handle'=108
'atom-ref'=109 'message'=109
'interface-ref'=110 'atom-ref'=110
'optional'=111 'interface-ref'=111
'list'=112 'optional'=112
'record'=113 'list'=113
'bool'=114 'record'=114
'bytes'=115 'bool'=115
'double'=116 'bytes'=116
'int32'=117 'double'=117
'int64'=118 'int32'=118
'string'=119 'int64'=119
'uint32'=120 'string'=120
'uint64'=121 'uint32'=121
'true'=122 'uint64'=122
'false'=123 'true'=123
'null'=124 'false'=124
'->'=125 'null'=125
':'=126 '->'=126
';'=127 ':'=127
','=128 ';'=128
'.'=129 ','=129
'{'=130 '.'=130
'}'=131 '{'=131
'['=132 '}'=132
']'=133 '['=133
'('=134 ']'=134
')'=135 '('=135
'<'=136 ')'=136
'>'=137 '<'=137
'&'=138 '>'=138
'='=139 '&'=139
'='=140
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -34,6 +34,7 @@ import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
import { QueryClauseContext } from "./QuixosCapabilityParser.js"; import { QueryClauseContext } from "./QuixosCapabilityParser.js";
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js"; import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js"; import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
import { AcceptanceClauseContext } from "./QuixosCapabilityParser.js";
import { ExecutionClauseContext } from "./QuixosCapabilityParser.js"; import { ExecutionClauseContext } from "./QuixosCapabilityParser.js";
import { SelectedOperationsContext } from "./QuixosCapabilityParser.js"; import { SelectedOperationsContext } from "./QuixosCapabilityParser.js";
import { PackageConstructorExportContext } from "./QuixosCapabilityParser.js"; import { PackageConstructorExportContext } from "./QuixosCapabilityParser.js";
@@ -62,6 +63,7 @@ import { OperationBindingDeclContext } from "./QuixosCapabilityParser.js";
import { MemberOperationRefContext } from "./QuixosCapabilityParser.js"; import { MemberOperationRefContext } from "./QuixosCapabilityParser.js";
import { OperationNameContext } from "./QuixosCapabilityParser.js"; import { OperationNameContext } from "./QuixosCapabilityParser.js";
import { OperationProviderContext } from "./QuixosCapabilityParser.js"; import { OperationProviderContext } from "./QuixosCapabilityParser.js";
import { AcceptanceBindingContext } from "./QuixosCapabilityParser.js";
import { StatePrimitiveContext } from "./QuixosCapabilityParser.js"; import { StatePrimitiveContext } from "./QuixosCapabilityParser.js";
import { EdgePrimitiveContext } from "./QuixosCapabilityParser.js"; import { EdgePrimitiveContext } from "./QuixosCapabilityParser.js";
import { DependencyBindingBlockContext } from "./QuixosCapabilityParser.js"; import { DependencyBindingBlockContext } from "./QuixosCapabilityParser.js";
@@ -279,6 +281,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitPackageFunctionExport?: (ctx: PackageFunctionExportContext) => Result; visitPackageFunctionExport?: (ctx: PackageFunctionExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.acceptanceClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitAcceptanceClause?: (ctx: AcceptanceClauseContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.executionClause`. * Visit a parse tree produced by `QuixosCapabilityParser.executionClause`.
* @param ctx the parse tree * @param ctx the parse tree
@@ -447,6 +455,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitOperationProvider?: (ctx: OperationProviderContext) => Result; visitOperationProvider?: (ctx: OperationProviderContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.acceptanceBinding`.
* @param ctx the parse tree
* @return the visitor result
*/
visitAcceptanceBinding?: (ctx: AcceptanceBindingContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.statePrimitive`. * Visit a parse tree produced by `QuixosCapabilityParser.statePrimitive`.
* @param ctx the parse tree * @param ctx the parse tree
+40
View File
@@ -1514,6 +1514,13 @@ const lowerPackage = (
const operation = exportContext.packageOperationExport(); const operation = exportContext.packageOperationExport();
const generic = operation ?? exportContext.packageFunctionExport(); const generic = operation ?? exportContext.packageFunctionExport();
if (generic?.typeParameters()) { if (generic?.typeParameters()) {
if (generic.acceptanceClause())
loweringIssue(
state,
generic,
"unsupported-generic-acceptance",
"Acceptance hooks currently require a closed export",
);
genericExports.push(atGenericSource(generic, () => lowerGenericPackageExport(state, generic))); genericExports.push(atGenericSource(generic, () => lowerGenericPackageExport(state, generic)));
return []; return [];
} }
@@ -1535,6 +1542,22 @@ const lowerPackage = (
state.types.self = previousSelf; state.types.self = previousSelf;
} }
}); });
// 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;
}
for (const entry of context.packageExport()) { for (const entry of context.packageExport()) {
const specialized = entry.packageQuerySpecialization(); const specialized = entry.packageQuerySpecialization();
if (!specialized) continue; if (!specialized) continue;
@@ -2047,6 +2070,22 @@ const lowerConformance = (
} else if (provider.typeArguments()) { } else if (provider.typeArguments()) {
throw new GenericTypeError("type-arity", exportName, "Non-generic export takes no type arguments"); throw new GenericTypeError("type-arity", exportName, "Non-generic export takes no type arguments");
} }
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;
return packageSymbol && exportSymbol return packageSymbol && exportSymbol
? [ ? [
{ {
@@ -2056,6 +2095,7 @@ const lowerConformance = (
kind: "package" as const, kind: "package" as const,
packageRevisionId: packageSymbol.revisionId, packageRevisionId: packageSymbol.revisionId,
exportId: exportSymbol.exportId, exportId: exportSymbol.exportId,
...(acceptanceDependencies ? { acceptanceDependencies } : {}),
dependencies: lowerBoundDependencies( dependencies: lowerBoundDependencies(
state, state,
provider.dependencyBindingBlock() ?? undefined, provider.dependencyBindingBlock() ?? undefined,
+3
View File
@@ -257,6 +257,8 @@ export interface DependencyPort {
} }
interface PackageExportBase { interface PackageExportBase {
/** Required authoritative snapshot export in this package. */
acceptanceExportId?: PackageExportId;
execution?: ExecutionContract; execution?: ExecutionContract;
id: PackageExportId; id: PackageExportId;
displayName: string; displayName: string;
@@ -375,6 +377,7 @@ export type Binding =
packageRevisionId: PackageRevisionId; packageRevisionId: PackageRevisionId;
exportId: PackageExportId; exportId: PackageExportId;
dependencies: BoundDependency[]; dependencies: BoundDependency[];
acceptanceDependencies?: BoundDependency[];
}; };
export interface OperationBinding { export interface OperationBinding {
+40
View File
@@ -961,6 +961,32 @@ const validatePackages = (
const exportPath = `${path}.exports[${exportIndex}]`; const exportPath = `${path}.exports[${exportIndex}]`;
validateValueType(issues, entry.inputType, `${exportPath}.inputType`, indexes); validateValueType(issues, entry.inputType, `${exportPath}.inputType`, indexes);
validateValueType(issues, entry.outputType, `${exportPath}.outputType`, indexes); validateValueType(issues, entry.outputType, `${exportPath}.outputType`, indexes);
if (entry.acceptanceExportId !== undefined) {
const check = revision.exports.find((candidate) => candidate.id === entry.acceptanceExportId);
if (
entry.execution?.kind !== "transaction" ||
!entry.execution.targets.includes("server") ||
!check ||
check.execution?.kind !== "snapshot" ||
check.execution.targets.length !== 1 ||
check.execution.targets[0] !== "server" ||
check.acceptanceExportId !== undefined ||
entry.kind !== check.kind ||
entry.kind === "constructor" ||
!valueTypesEqual(entry.inputType, check.inputType) ||
!valueTypesEqual(check.outputType, valueType.unit) ||
(entry.kind === "operation" &&
(check.kind !== "operation" ||
check.mode !== "call" ||
!isDeepStrictEqual(entry.receiverRequirement, check.receiverRequirement)))
)
issue(
issues,
"invalid-operation",
`${exportPath}.acceptanceExportId`,
"Acceptance requires a server snapshot export with matching kind, receiver and input, unit output, and no further acceptance hook",
);
}
if (entry.kind === "operation") { if (entry.kind === "operation") {
validateOperationEventShape(issues, entry, exportPath); validateOperationEventShape(issues, entry, exportPath);
if (entry.eventType) { if (entry.eventType) {
@@ -1862,6 +1888,20 @@ const validateConformances = (
requirementGraph, requirementGraph,
graphSourceKey: key, graphSourceKey: key,
}); });
const check = packageEntry.exports.get(packageExport.acceptanceExportId!);
if (check) {
validateBoundDependencies(issues, {
dependencies: binding.acceptanceDependencies ?? [],
dependencyPorts: check.dependencyPorts,
atomId: conformance.atomId,
conformance,
path: `${bindingPath}.binding.acceptance`,
indexes,
requirementGraph,
graphSourceKey: key,
});
} else if (binding.acceptanceDependencies !== undefined)
issue(issues, "invalid-package-binding", bindingPath, "Acceptance ports require a declared acceptance export");
} }
for (const member of interfaceEntry.revision.members) { for (const member of interfaceEntry.revision.members) {
+67 -51
View File
@@ -3,6 +3,8 @@ import type {
InitializationField, InitializationField,
PersistentAttachment, PersistentAttachment,
DependencyBinding, DependencyBinding,
BoundDependency,
PackageExport,
} from "../capability-model/types.js"; } from "../capability-model/types.js";
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js"; import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { portableValueSchema } from "../bindings/portable.js"; import { portableValueSchema } from "../bindings/portable.js";
@@ -107,57 +109,70 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
else { else {
const pkg = workspace.packageImports.find((p) => p.revisionId === binding.packageRevisionId)!; const pkg = workspace.packageImports.find((p) => p.revisionId === binding.packageRevisionId)!;
const entry = pkg.exports.find((e) => e.id === binding.exportId)!; const entry = pkg.exports.find((e) => e.id === binding.exportId)!;
const ports = binding.dependencies.map((dependency) => { const compilePorts = (entry: PackageExport, dependencies: BoundDependency[]) =>
const requirement = entry.dependencyPorts.find((p) => p.id === dependency.portId)!.requirement; dependencies.map((dependency) => {
const bound = dependency.binding; const requirement = entry.dependencyPorts.find((p) => p.id === dependency.portId)!.requirement;
const common = { id: dependency.portId, ...via(bound) }; const bound = dependency.binding;
if (bound.kind === "state" && requirement.kind === "state") const common = { id: dependency.portId, ...via(bound) };
return { if (bound.kind === "state" && requirement.kind === "state")
...common, return {
kind: "state", ...common,
owner: slotOwner(bound.slotId), kind: "state",
slot: bound.slotId, owner: slotOwner(bound.slotId),
primitives: requirement.primitives, slot: bound.slotId,
}; primitives: requirement.primitives,
if (bound.kind === "edge" && requirement.kind === "edge") };
return { if (bound.kind === "edge" && requirement.kind === "edge")
...common, return {
kind: "edge", ...common,
edge: bound.edgeTypeId, kind: "edge",
projection: bound.projectionId, edge: bound.edgeTypeId,
primitives: requirement.primitives, projection: bound.projectionId,
}; primitives: requirement.primitives,
if (bound.kind === "interface" && requirement.kind === "interface") { };
const iface = workspace.interfaceImports.find((i) => i.revisionId === bound.interfaceRevisionId)!; if (bound.kind === "interface" && requirement.kind === "interface") {
return { const iface = workspace.interfaceImports.find((i) => i.revisionId === bound.interfaceRevisionId)!;
...common, return {
kind: "interface", ...common,
interface: bound.interfaceRevisionId, kind: "interface",
operations: requirement.operations ?? iface.members.flatMap((m) => m.operations.map((o) => o.id)), interface: bound.interfaceRevisionId,
}; operations: requirement.operations ?? iface.members.flatMap((m) => m.operations.map((o) => o.id)),
} };
if (bound.kind === "creation" && requirement.kind === "creation") }
return { if (bound.kind === "creation" && requirement.kind === "creation")
...common, return {
kind: "creation", ...common,
atom: bound.atomId, kind: "creation",
schema: portableValueSchema(requirement.inputType), atom: bound.atomId,
recipe: hexDigest({ plan: plan.digest, operation: operation.key, port: dependency.portId }), schema: portableValueSchema(requirement.inputType),
owners: bound.owners.map((seed) => { recipe: hexDigest({ plan: plan.digest, operation: operation.key, port: dependency.portId }),
const owner = owners.find((o) => o.atom === bound.atomId && o.interface === seed.interfaceRevisionId); owners: bound.owners.map((seed) => {
if (!owner) const owner = owners.find((o) => o.atom === bound.atomId && o.interface === seed.interfaceRevisionId);
throw new ExecutionPlanError( if (!owner)
"MISSING_STORAGE_OWNER", throw new ExecutionPlanError(
dependency.portId, "MISSING_STORAGE_OWNER",
"Creation has no initialization owner", dependency.portId,
); "Creation has no initialization owner",
return { owner: owner.id, inputPath: seed.inputPath }; );
}), return { owner: owner.id, inputPath: seed.inputPath };
}; }),
unsupported = `UNSUPPORTED_EXECUTION_PORT:${bound.kind}`; };
return { ...common, kind: "unavailable" }; unsupported = `UNSUPPORTED_EXECUTION_PORT:${bound.kind}`;
}); return { ...common, kind: "unavailable" };
target = { kind: "package", package: binding.packageRevisionId, export: binding.exportId, ports }; });
const ports = compilePorts(entry, binding.dependencies);
const check = entry.acceptanceExportId
? pkg.exports.find((candidate) => candidate.id === entry.acceptanceExportId)!
: undefined;
target = {
kind: "package",
package: binding.packageRevisionId,
export: binding.exportId,
ports,
...(check
? { acceptance: { export: check.id, ports: compilePorts(check, binding.acceptanceDependencies ?? []) } }
: {}),
};
} }
const input = schema(operation.input), const input = schema(operation.input),
output = schema(operation.output); output = schema(operation.output);
@@ -179,6 +194,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
revision: pkg.revisionId, revision: pkg.revisionId,
exports: pkg.exports.map((entry) => ({ exports: pkg.exports.map((entry) => ({
id: entry.id, id: entry.id,
...(entry.acceptanceExportId ? { acceptance: entry.acceptanceExportId } : {}),
effect: entry.execution?.kind ?? "remote", effect: entry.execution?.kind ?? "remote",
targets: entry.execution && entry.execution.kind !== "remote" ? entry.execution.targets : [], targets: entry.execution && entry.execution.kind !== "remote" ? entry.execution.targets : [],
receiver: receiver:
+41
View File
@@ -0,0 +1,41 @@
import assert from "node:assert/strict";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
const contract = `interface Limit id "limit" revision "limit@1" {
operation change id "change" : int32 -> unit {
call id "change:call" execution transaction targets [browser, server];
}
}`;
export const runtime = `external atom Item id "item";
package Runtime id "runtime" revision "runtime@1" {
operation change id "runtime:change" : int32 -> unit mode call execution transaction targets [browser, server]
receiver atom Item requires { state level id "write:value" : int32 [write]; } accept using check;
operation check id "runtime:check" : int32 -> unit mode call execution snapshot targets [server]
receiver atom Item requires {
state level id "read:value" : int32 [read];
state ceiling id "read:ceiling" : int32 [read];
};
}`;
export const workspace = `workspace LimitTest id "workspace:limit" revision "workspace:limit@1" commit "${"a".repeat(40)}" {
import interface Limit;
import package Runtime;
atom Item id "item";
conform Item as Limit id "item:limit" {
private state Value id "value" on Item : int32 policy register default 0;
private state Ceiling id "ceiling" on Item : int32 policy register default 100;
bind change.call to package Runtime.change with { level to state Value; }
accept with { level to state Value; ceiling to state Ceiling; };
}
}`;
export function compile(packageSource = runtime, workspaceSource = workspace) {
const source = { repository: "https://example.test/limit.git", commit: "a".repeat(40) };
const iface = compileCapabilityResourceSource(contract, { source });
assert.ok(iface.ok && iface.resource.kind === "interface", JSON.stringify(iface.diagnostics));
const pkg = compileCapabilityResourceSource(packageSource, { source });
if (!pkg.ok) return pkg;
assert.equal(pkg.resource.kind, "package");
if (pkg.resource.kind !== "package") throw Error("Expected package");
return compileCapabilitySource(workspaceSource, "workspace.qx", {
interfaces: new Map([["Limit", iface.resource.revision]]),
packages: new Map([["Runtime", pkg.resource.revision]]),
});
}
+70
View File
@@ -0,0 +1,70 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/parser.js";
import { formatQx } from "../src/capability-language/source.js";
import { compileExecutionPlan, compileKernelProgram, checkExecutionWorld } from "../src/execution-world/index.js";
import { generatePortableTypeScriptBindings } from "../src/bindings/portable.js";
import { buildArtifactFixture } from "./artifact-fixture.js";
import { compile, runtime, workspace } from "./acceptance-fixture.js";
test("acceptance syntax retains separate final-state ports through formatting, planning, adapters and artifacts", async () => {
const result = compile();
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const formatted = compile(formatQx(runtime), formatQx(workspace));
assert.ok(formatted.ok, JSON.stringify(formatted.diagnostics));
const plan = compileExecutionPlan(result.workspace);
assert.equal(compileExecutionPlan(formatted.workspace).digest, plan.digest);
const kernel = compileKernelProgram(plan) as {
operations: { binding: { ports: { id: string }[]; acceptance: { export: string; ports: { id: string }[] } } }[];
packages: { exports: { id: string; acceptance?: string }[] }[];
};
assert.deepEqual(
kernel.operations[0]!.binding.ports.map((p) => p.id),
["write:value"],
);
assert.equal(kernel.operations[0]!.binding.acceptance.export, "runtime:check");
assert.deepEqual(kernel.operations[0]!.binding.acceptance.ports.map((p) => p.id).sort(), [
"read:ceiling",
"read:value",
]);
assert.equal(kernel.packages[0]!.exports.find((e) => e.id === "runtime:change")!.acceptance, "runtime:check");
assert.ok(
!generatePortableTypeScriptBindings(plan.definition.bindingSchema, "runtime@1", { target: "browser" }).includes(
'"runtime:check"',
),
);
assert.ok(
generatePortableTypeScriptBindings(plan.definition.bindingSchema, "runtime@1", { target: "server" }).includes(
'"runtime:check"',
),
);
const artifacts = buildArtifactFixture(plan, "runtime@1");
await checkExecutionWorld(plan, [artifacts.manifest], artifacts.read);
});
test("acceptance declarations reject wrong effects/signatures and missing or mutable ports", () => {
for (const [pkg, ws] of [
[runtime.replace("snapshot targets [server]", "transaction targets [server]"), workspace],
[runtime.replace("snapshot targets [server]", "snapshot targets [browser, server]"), workspace],
[
runtime.replace('check id "runtime:check" : int32 -> unit', 'check id "runtime:check" : string -> unit'),
workspace,
],
[
runtime.replace('check id "runtime:check" : int32 -> unit', 'check id "runtime:check" : int32 -> int32'),
workspace,
],
[runtime.replace('id "read:ceiling" : int32 [read]', 'id "read:ceiling" : int32 [write]'), workspace],
[runtime, workspace.replace("ceiling to state Ceiling;", "")],
[runtime.replace(" accept using check", ""), workspace],
])
assert.equal(compile(pkg, ws).ok, false, `${pkg}\n${ws}`);
});
test("generic acceptance syntax rejects explicitly until specialization is supported", () => {
const result = compileCapabilityResourceSource(
runtime.replace("operation change id", "operation change<value T> id"),
{
source: { repository: "https://example.test/limit.git", commit: "a".repeat(40) },
},
);
assert.equal(result.ok, false);
assert.ok(result.diagnostics.some((d) => d.code === "unsupported-generic-acceptance"));
});
+3 -1
View File
@@ -21,7 +21,9 @@ export const buildArtifactFixture = (plan: ExecutionPlan, revision = plan.defini
path, path,
ports: "quixos-transaction-ports-v1" as const, ports: "quixos-transaction-ports-v1" as const,
...(compatibility === "quixos-js-module-v1" ? { registryExport: "handlers" } : {}), ...(compatibility === "quixos-js-module-v1" ? { registryExport: "handlers" } : {}),
exports: expected.exports.map((e) => e.id), exports: expected.exports
.filter((e) => target === "server" || (e.execution.kind !== "remote" && e.execution.targets.includes(target)))
.map((e) => e.id),
files: [ files: [
{ {
path, path,
+7
View File
@@ -89,6 +89,13 @@ void negative;
); );
const { asyncObjectPlan } = await import("./async-fixture.js"); const { asyncObjectPlan } = await import("./async-fixture.js");
const { compile: compileAcceptance } = await import("./acceptance-fixture.js");
const acceptance = compileAcceptance();
if (!acceptance.ok) throw Error(JSON.stringify(acceptance.diagnostics));
const acceptancePlan = compileExecutionPlan(acceptance.workspace);
await mkdir("dist/portable-reader/acceptance", { recursive: true });
await writeFile("dist/portable-reader/acceptance/plan.json", JSON.stringify(acceptancePlan));
await writeFile("dist/portable-reader/acceptance/kernel.json", JSON.stringify(compileKernelProgram(acceptancePlan)));
const { recurringTaskPlan } = await import("./recurring-fixture.js"); const { recurringTaskPlan } = await import("./recurring-fixture.js");
const recurringPlan = await recurringTaskPlan(); const recurringPlan = await recurringTaskPlan();
await mkdir("dist/portable-reader/recurring", { recursive: true }); await mkdir("dist/portable-reader/recurring", { recursive: true });