Implement main-thread Camino sessions and synchronous CRDT editing

This commit is contained in:
Timothy J. Aveni
2026-09-27 23:57:29 -07:00
parent ad72185a6c
commit 696023b4ba
24 changed files with 3784 additions and 3218 deletions
+13 -2
View File
@@ -189,18 +189,22 @@ fragmentVariable : identifier TO identifier SEMI;
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? acceptanceClause? SEMI MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? preparationClause? acceptanceClause? SEMI
; ;
packageFunctionExport packageFunctionExport
: FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType : FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
executionClause? dependencyBlock? acceptanceClause? SEMI executionClause? dependencyBlock? preparationClause? acceptanceClause? SEMI
; ;
acceptanceClause acceptanceClause
: ACCEPT USING identifier : ACCEPT USING identifier
; ;
preparationClause
: PREPARES CREATION identifier USING stringLiteral
;
executionClause executionClause
: EXECUTION REMOTE : EXECUTION REMOTE
| EXECUTION (SNAPSHOT | TRANSACTION) TARGETS LBRACK identifierList RBRACK | EXECUTION (SNAPSHOT | TRANSACTION) TARGETS LBRACK identifierList RBRACK
@@ -418,6 +422,7 @@ valueType
| OPTIONAL LT valueType GT | OPTIONAL LT valueType GT
| LIST LT valueType GT | LIST LT valueType GT
| RECORD LBRACE recordField* RBRACE | RECORD LBRACE recordField* RBRACE
| VARIANT ID stringLiteral LBRACE variantCase+ RBRACE
| identifier typeArguments? | identifier typeArguments?
; ;
@@ -425,6 +430,10 @@ recordField
: identifier COLON valueType SEMI : identifier COLON valueType SEMI
; ;
variantCase
: identifier ID stringLiteral COLON valueType SEMI
;
scalarType scalarType
: BOOL : BOOL
| BYTES | BYTES
@@ -517,6 +526,8 @@ PRESENTATION: 'presentation';
SUBJECT: 'subject'; SUBJECT: 'subject';
FOR: 'for'; FOR: 'for';
CREATION: 'creation'; CREATION: 'creation';
VARIANT: 'variant';
PREPARES: 'prepares';
INITIALIZE: 'initialize'; INITIALIZE: 'initialize';
WORKSPACE: 'workspace'; WORKSPACE: 'workspace';
EXECUTION: 'execution'; EXECUTION: 'execution';
+2
View File
@@ -11,6 +11,8 @@ export const generateClientContracts = (
throw new Error("Host contracts require closed interface applications, not generic definitions"); throw new Error("Host contracts require closed interface applications, not generic definitions");
const type = (value: ValueType): string => { const type = (value: ValueType): string => {
switch (value.kind) { switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin": case "builtin":
return value.name === "unit" ? "undefined" : "string"; return value.name === "unit" ? "undefined" : "string";
case "scalar": case "scalar":
+2
View File
@@ -49,6 +49,8 @@ export const genericImplementationType = (
const value = (type: ValueTypeExpression, scope: Scope, depth = 0): string => { const value = (type: ValueTypeExpression, scope: Scope, depth = 0): string => {
if (depth > 128) throw new Error("Generic type expansion exceeds depth limit"); if (depth > 128) throw new Error("Generic type expansion exceeds depth limit");
switch (type.kind) { switch (type.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "parameter": { case "parameter": {
const bound = scope.arguments.get(type.parameterId); const bound = scope.arguments.get(type.parameterId);
if (bound) return bound; if (bound) return bound;
+3
View File
@@ -112,6 +112,8 @@ export const generateTypeScriptBindings = (
const messages = new Map<string, string>(); const messages = new Map<string, string>();
const type = (value: ValueType): string => { const type = (value: ValueType): string => {
switch (value.kind) { switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin": case "builtin":
return value.name === "unit" ? "null" : "QxWatchHandle"; return value.name === "unit" ? "null" : "QxWatchHandle";
case "scalar": case "scalar":
@@ -340,6 +342,7 @@ export const generateTypeScriptBindings = (
// A used port still fails above if its own required codec is missing. // A used port still fails above if its own required codec is missing.
const hasCodec = (value: ValueType): boolean => { const hasCodec = (value: ValueType): boolean => {
if (value.kind === "message") return !!options.messages?.[value.descriptorId]; if (value.kind === "message") return !!options.messages?.[value.descriptorId];
if (value.kind === "variant") return false;
if (value.kind === "record") return Object.values(value.fields).every(hasCodec); if (value.kind === "record") return Object.values(value.fields).every(hasCodec);
if (value.kind === "list" || value.kind === "optional") return hasCodec(value.value); if (value.kind === "list" || value.kind === "optional") return hasCodec(value.value);
return true; return true;
+15
View File
@@ -29,6 +29,17 @@ const expectation = (e: ObjectExpectation) => ({
/** The protocol IR and replica wire schema intentionally share structure, not reference identity. */ /** The protocol IR and replica wire schema intentionally share structure, not reference identity. */
export function portableValueSchema(type: ValueType): unknown { export function portableValueSchema(type: ValueType): unknown {
switch (type.kind) { switch (type.kind) {
case "variant":
return {
kind: "variant",
id: type.id,
cases: Object.fromEntries(
Object.entries(type.cases).map(([label, entry]) => [
label,
{ id: entry.id, value: portableValueSchema(entry.value) },
]),
),
};
case "change": case "change":
return { return {
kind: "record", kind: "record",
@@ -65,6 +76,10 @@ export function portableValueSchema(type: ValueType): unknown {
export function tsType(type: ValueType): string { export function tsType(type: ValueType): string {
portableValueSchema(type); portableValueSchema(type);
switch (type.kind) { switch (type.kind) {
case "variant":
return `(${Object.entries(type.cases)
.map(([label, entry]) => `{ readonly kind: ${q(label)}; readonly value: ${tsType(entry.value)} }`)
.join(" | ")})`;
case "change": case "change":
return `PreparedChange<${tsType(type.value)}>`; return `PreparedChange<${tsType(type.value)}>`;
case "scalar": case "scalar":
+3
View File
@@ -15,6 +15,8 @@ export function generateReactBindings(
const declarations: string[] = []; const declarations: string[] = [];
const type = (value: ValueType): string => { const type = (value: ValueType): string => {
switch (value.kind) { switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin": case "builtin":
if (value.name !== "unit") throw new Error(`Unsupported React props builtin ${value.name}`); if (value.name !== "unit") throw new Error(`Unsupported React props builtin ${value.name}`);
return "null"; return "null";
@@ -85,6 +87,7 @@ export function generateReactBindings(
const browserValue = (value: ValueType, seen: Set<string>): boolean => { const browserValue = (value: ValueType, seen: Set<string>): boolean => {
if (value.kind === "message") return false; if (value.kind === "message") return false;
if (value.kind === "builtin") return value.name === "unit"; if (value.kind === "builtin") return value.name === "unit";
if (value.kind === "variant") return false;
if (value.kind === "record") return Object.values(value.fields).every((field) => browserValue(field, seen)); if (value.kind === "record") return Object.values(value.fields).every((field) => browserValue(field, seen));
if (value.kind === "list" || value.kind === "optional") return browserValue(value.value, seen); if (value.kind === "list" || value.kind === "optional") return browserValue(value.value, seen);
if (value.kind === "object-ref" && value.expectation.kind === "interface") { if (value.kind === "object-ref" && value.expectation.kind === "interface") {
File diff suppressed because one or more lines are too long
@@ -5,152 +5,154 @@ PRESENTATION=4
SUBJECT=5 SUBJECT=5
FOR=6 FOR=6
CREATION=7 CREATION=7
INITIALIZE=8 VARIANT=8
WORKSPACE=9 PREPARES=9
EXECUTION=10 INITIALIZE=10
SNAPSHOT=11 WORKSPACE=11
TRANSACTION=12 EXECUTION=12
REMOTE=13 SNAPSHOT=13
TARGETS=14 TRANSACTION=14
PORTABLE=15 REMOTE=15
REPLICA=16 TARGETS=16
LOCAL=17 PORTABLE=17
FROM=18 REPLICA=18
RESULT_BYTES=19 LOCAL=19
RPC_CALLS=20 FROM=20
DEADLINE_MS=21 RESULT_BYTES=21
QUERY=22 RPC_CALLS=22
REFLECTION=23 DEADLINE_MS=23
SPECIALIZE=24 QUERY=24
ROOT=25 REFLECTION=25
DOCUMENT=26 SPECIALIZE=26
FRAGMENTS=27 ROOT=27
VIEW=28 DOCUMENT=28
MAX=29 FRAGMENTS=29
ALLOW=30 VIEW=30
POLL=31 MAX=31
QUERYABLE=32 ALLOW=32
RPC=33 POLL=33
QUERY_REASON=34 QUERYABLE=34
TYPE=35 RPC=35
OBJECT=36 QUERY_REASON=36
STORABLE=37 TYPE=37
IMPLEMENTS=38 OBJECT=38
REF=39 STORABLE=39
FRAGMENT=40 IMPLEMENTS=40
IMPORT=41 REF=41
EXTERNAL=42 FRAGMENT=42
ATOM=43 IMPORT=43
INTERFACE=44 EXTERNAL=44
INTERFACES=45 ATOM=45
PACKAGE=46 INTERFACE=46
VALUE=47 INTERFACES=47
RELATION=48 PACKAGE=48
OPERATION=49 VALUE=49
FUNCTION=50 RELATION=50
CONSTRUCTOR=51 OPERATION=51
CONSTRUCTS=52 FUNCTION=52
INPUT=53 CONSTRUCTOR=53
CONFORM=54 CONSTRUCTS=54
AS=55 INPUT=55
BIND=56 CONFORM=56
STATIC=57 AS=57
TO=58 BIND=58
PRIVATE=59 STATIC=59
SHARED=60 TO=60
STATE=61 PRIVATE=61
EDGE=62 SHARED=62
PROJECTION=63 STATE=63
WITH=64 EDGE=64
USING=65 PROJECTION=65
ACCEPT=66 WITH=66
VIA=67 USING=67
MATERIALIZE=68 ACCEPT=68
IF=69 VIA=69
ABSENT=70 MATERIALIZE=70
ON=71 IF=71
POLICY=72 ABSENT=72
DEFAULT=73 ON=73
SOURCE=74 POLICY=74
REPOSITORY=75 DEFAULT=75
COMMIT=76 SOURCE=76
REVISION=77 REPOSITORY=77
SEMANTIC_MAJOR=78 COMMIT=78
ON_DELETE=79 REVISION=79
LIFECYCLE=80 SEMANTIC_MAJOR=80
RETAIN_OTHER=81 ON_DELETE=81
KEYED=82 LIFECYCLE=82
PUBLIC_TRAVERSAL=83 RETAIN_OTHER=83
ID=84 KEYED=84
DOC=85 PUBLIC_TRAVERSAL=85
MODE=86 ID=86
EMITS=87 DOC=87
RECEIVER=88 MODE=88
REQUIRES=89 EMITS=89
ANY=90 RECEIVER=90
GET=91 REQUIRES=91
SET=92 ANY=92
WATCH=93 GET=93
START=94 SET=94
STOP=95 WATCH=95
READ=96 START=96
WRITE=97 STOP=97
RESOLVE=98 READ=98
CONNECT=99 WRITE=99
DISCONNECT=100 RESOLVE=100
CALL=101 CONNECT=101
WATCH_START=102 DISCONNECT=102
WATCH_STOP=103 CALL=103
SUBSCRIBE=104 WATCH_START=104
UNSUBSCRIBE=105 WATCH_STOP=105
REGISTER=106 SUBSCRIBE=106
CRDT=107 UNSUBSCRIBE=107
OPTIONAL_ONE=108 REGISTER=108
EXACTLY_ONE=109 CRDT=109
MANY_UNIQUE=110 OPTIONAL_ONE=110
MANY=111 EXACTLY_ONE=111
ORDERED=112 MANY_UNIQUE=112
UNIT=113 MANY=113
WATCH_HANDLE=114 ORDERED=114
MESSAGE=115 UNIT=115
ATOM_REF=116 WATCH_HANDLE=116
INTERFACE_REF=117 MESSAGE=117
OPTIONAL=118 ATOM_REF=118
LIST=119 INTERFACE_REF=119
RECORD=120 OPTIONAL=120
BOOL=121 LIST=121
BYTES=122 RECORD=122
DOUBLE=123 BOOL=123
INT32=124 BYTES=124
INT64=125 DOUBLE=125
STRING=126 INT32=126
UINT32=127 INT64=127
UINT64=128 STRING=128
TRUE=129 UINT32=129
FALSE=130 UINT64=130
NULL=131 TRUE=131
ARROW=132 FALSE=132
COLON=133 NULL=133
SEMI=134 ARROW=134
COMMA=135 COLON=135
DOT=136 SEMI=136
LBRACE=137 COMMA=137
RBRACE=138 DOT=138
LBRACK=139 LBRACE=139
RBRACK=140 RBRACE=140
LPAREN=141 LBRACK=141
RPAREN=142 RBRACK=142
LT=143 LPAREN=143
GT=144 RPAREN=144
AMP=145 LT=145
EQUAL=146 GT=146
INTEGER=147 AMP=147
JSON_NUMBER=148 EQUAL=148
IDENTIFIER=149 INTEGER=149
STRING_LITERAL=150 JSON_NUMBER=150
LINE_COMMENT=151 IDENTIFIER=151
BLOCK_COMMENT=152 STRING_LITERAL=152
WS=153 LINE_COMMENT=153
BLOCK_COMMENT=154
WS=155
'edit'=1 'edit'=1
'module'=2 'module'=2
'contract'=3 'contract'=3
@@ -158,142 +160,144 @@ WS=153
'subject'=5 'subject'=5
'for'=6 'for'=6
'creation'=7 'creation'=7
'initialize'=8 'variant'=8
'workspace'=9 'prepares'=9
'execution'=10 'initialize'=10
'snapshot'=11 'workspace'=11
'transaction'=12 'execution'=12
'remote'=13 'snapshot'=13
'targets'=14 'transaction'=14
'portable'=15 'remote'=15
'replica'=16 'targets'=16
'local'=17 'portable'=17
'from'=18 'replica'=18
'result-bytes'=19 'local'=19
'rpc-calls'=20 'from'=20
'deadline-ms'=21 'result-bytes'=21
'query'=22 'rpc-calls'=22
'reflection'=23 'deadline-ms'=23
'specialize'=24 'query'=24
'root'=25 'reflection'=25
'document'=26 'specialize'=26
'fragments'=27 'root'=27
'view'=28 'document'=28
'max'=29 'fragments'=29
'allow'=30 'view'=30
'poll'=31 'max'=31
'queryable'=32 'allow'=32
'rpc'=33 'poll'=33
'query-reason'=34 'queryable'=34
'type'=35 'rpc'=35
'object'=36 'query-reason'=36
'storable'=37 'type'=37
'implements'=38 'object'=38
'ref'=39 'storable'=39
'fragment'=40 'implements'=40
'import'=41 'ref'=41
'external'=42 'fragment'=42
'atom'=43 'import'=43
'interface'=44 'external'=44
'interfaces'=45 'atom'=45
'package'=46 'interface'=46
'value'=47 'interfaces'=47
'relation'=48 'package'=48
'operation'=49 'value'=49
'function'=50 'relation'=50
'constructor'=51 'operation'=51
'constructs'=52 'function'=52
'input'=53 'constructor'=53
'conform'=54 'constructs'=54
'as'=55 'input'=55
'bind'=56 'conform'=56
'static'=57 'as'=57
'to'=58 'bind'=58
'private'=59 'static'=59
'shared'=60 'to'=60
'state'=61 'private'=61
'edge'=62 'shared'=62
'projection'=63 'state'=63
'with'=64 'edge'=64
'using'=65 'projection'=65
'accept'=66 'with'=66
'via'=67 'using'=67
'materialize'=68 'accept'=68
'if'=69 'via'=69
'absent'=70 'materialize'=70
'on'=71 'if'=71
'policy'=72 'absent'=72
'default'=73 'on'=73
'source'=74 'policy'=74
'repository'=75 'default'=75
'commit'=76 'source'=76
'revision'=77 'repository'=77
'semantic-major'=78 'commit'=78
'on-delete'=79 'revision'=79
'lifecycle'=80 'semantic-major'=80
'retain-other'=81 'on-delete'=81
'keyed'=82 'lifecycle'=82
'public-traversal'=83 'retain-other'=83
'id'=84 'keyed'=84
'doc'=85 'public-traversal'=85
'mode'=86 'id'=86
'emits'=87 'doc'=87
'receiver'=88 'mode'=88
'requires'=89 'emits'=89
'any'=90 'receiver'=90
'get'=91 'requires'=91
'set'=92 'any'=92
'watch'=93 'get'=93
'start'=94 'set'=94
'stop'=95 'watch'=95
'read'=96 'start'=96
'write'=97 'stop'=97
'resolve'=98 'read'=98
'connect'=99 'write'=99
'disconnect'=100 'resolve'=100
'call'=101 'connect'=101
'watch-start'=102 'disconnect'=102
'watch-stop'=103 'call'=103
'subscribe'=104 'watch-start'=104
'unsubscribe'=105 'watch-stop'=105
'register'=106 'subscribe'=106
'crdt'=107 'unsubscribe'=107
'optional-one'=108 'register'=108
'exactly-one'=109 'crdt'=109
'many-unique'=110 'optional-one'=110
'many'=111 'exactly-one'=111
'ordered'=112 'many-unique'=112
'unit'=113 'many'=113
'watch-handle'=114 'ordered'=114
'message'=115 'unit'=115
'atom-ref'=116 'watch-handle'=116
'interface-ref'=117 'message'=117
'optional'=118 'atom-ref'=118
'list'=119 'interface-ref'=119
'record'=120 'optional'=120
'bool'=121 'list'=121
'bytes'=122 'record'=122
'double'=123 'bool'=123
'int32'=124 'bytes'=124
'int64'=125 'double'=125
'string'=126 'int32'=126
'uint32'=127 'int64'=127
'uint64'=128 'string'=128
'true'=129 'uint32'=129
'false'=130 'uint64'=130
'null'=131 'true'=131
'->'=132 'false'=132
':'=133 'null'=133
';'=134 '->'=134
','=135 ':'=135
'.'=136 ';'=136
'{'=137 ','=137
'}'=138 '.'=138
'['=139 '{'=139
']'=140 '}'=140
'('=141 '['=141
')'=142 ']'=142
'<'=143 '('=143
'>'=144 ')'=144
'&'=145 '<'=145
'='=146 '>'=146
'&'=147
'='=148
File diff suppressed because one or more lines are too long
@@ -5,152 +5,154 @@ PRESENTATION=4
SUBJECT=5 SUBJECT=5
FOR=6 FOR=6
CREATION=7 CREATION=7
INITIALIZE=8 VARIANT=8
WORKSPACE=9 PREPARES=9
EXECUTION=10 INITIALIZE=10
SNAPSHOT=11 WORKSPACE=11
TRANSACTION=12 EXECUTION=12
REMOTE=13 SNAPSHOT=13
TARGETS=14 TRANSACTION=14
PORTABLE=15 REMOTE=15
REPLICA=16 TARGETS=16
LOCAL=17 PORTABLE=17
FROM=18 REPLICA=18
RESULT_BYTES=19 LOCAL=19
RPC_CALLS=20 FROM=20
DEADLINE_MS=21 RESULT_BYTES=21
QUERY=22 RPC_CALLS=22
REFLECTION=23 DEADLINE_MS=23
SPECIALIZE=24 QUERY=24
ROOT=25 REFLECTION=25
DOCUMENT=26 SPECIALIZE=26
FRAGMENTS=27 ROOT=27
VIEW=28 DOCUMENT=28
MAX=29 FRAGMENTS=29
ALLOW=30 VIEW=30
POLL=31 MAX=31
QUERYABLE=32 ALLOW=32
RPC=33 POLL=33
QUERY_REASON=34 QUERYABLE=34
TYPE=35 RPC=35
OBJECT=36 QUERY_REASON=36
STORABLE=37 TYPE=37
IMPLEMENTS=38 OBJECT=38
REF=39 STORABLE=39
FRAGMENT=40 IMPLEMENTS=40
IMPORT=41 REF=41
EXTERNAL=42 FRAGMENT=42
ATOM=43 IMPORT=43
INTERFACE=44 EXTERNAL=44
INTERFACES=45 ATOM=45
PACKAGE=46 INTERFACE=46
VALUE=47 INTERFACES=47
RELATION=48 PACKAGE=48
OPERATION=49 VALUE=49
FUNCTION=50 RELATION=50
CONSTRUCTOR=51 OPERATION=51
CONSTRUCTS=52 FUNCTION=52
INPUT=53 CONSTRUCTOR=53
CONFORM=54 CONSTRUCTS=54
AS=55 INPUT=55
BIND=56 CONFORM=56
STATIC=57 AS=57
TO=58 BIND=58
PRIVATE=59 STATIC=59
SHARED=60 TO=60
STATE=61 PRIVATE=61
EDGE=62 SHARED=62
PROJECTION=63 STATE=63
WITH=64 EDGE=64
USING=65 PROJECTION=65
ACCEPT=66 WITH=66
VIA=67 USING=67
MATERIALIZE=68 ACCEPT=68
IF=69 VIA=69
ABSENT=70 MATERIALIZE=70
ON=71 IF=71
POLICY=72 ABSENT=72
DEFAULT=73 ON=73
SOURCE=74 POLICY=74
REPOSITORY=75 DEFAULT=75
COMMIT=76 SOURCE=76
REVISION=77 REPOSITORY=77
SEMANTIC_MAJOR=78 COMMIT=78
ON_DELETE=79 REVISION=79
LIFECYCLE=80 SEMANTIC_MAJOR=80
RETAIN_OTHER=81 ON_DELETE=81
KEYED=82 LIFECYCLE=82
PUBLIC_TRAVERSAL=83 RETAIN_OTHER=83
ID=84 KEYED=84
DOC=85 PUBLIC_TRAVERSAL=85
MODE=86 ID=86
EMITS=87 DOC=87
RECEIVER=88 MODE=88
REQUIRES=89 EMITS=89
ANY=90 RECEIVER=90
GET=91 REQUIRES=91
SET=92 ANY=92
WATCH=93 GET=93
START=94 SET=94
STOP=95 WATCH=95
READ=96 START=96
WRITE=97 STOP=97
RESOLVE=98 READ=98
CONNECT=99 WRITE=99
DISCONNECT=100 RESOLVE=100
CALL=101 CONNECT=101
WATCH_START=102 DISCONNECT=102
WATCH_STOP=103 CALL=103
SUBSCRIBE=104 WATCH_START=104
UNSUBSCRIBE=105 WATCH_STOP=105
REGISTER=106 SUBSCRIBE=106
CRDT=107 UNSUBSCRIBE=107
OPTIONAL_ONE=108 REGISTER=108
EXACTLY_ONE=109 CRDT=109
MANY_UNIQUE=110 OPTIONAL_ONE=110
MANY=111 EXACTLY_ONE=111
ORDERED=112 MANY_UNIQUE=112
UNIT=113 MANY=113
WATCH_HANDLE=114 ORDERED=114
MESSAGE=115 UNIT=115
ATOM_REF=116 WATCH_HANDLE=116
INTERFACE_REF=117 MESSAGE=117
OPTIONAL=118 ATOM_REF=118
LIST=119 INTERFACE_REF=119
RECORD=120 OPTIONAL=120
BOOL=121 LIST=121
BYTES=122 RECORD=122
DOUBLE=123 BOOL=123
INT32=124 BYTES=124
INT64=125 DOUBLE=125
STRING=126 INT32=126
UINT32=127 INT64=127
UINT64=128 STRING=128
TRUE=129 UINT32=129
FALSE=130 UINT64=130
NULL=131 TRUE=131
ARROW=132 FALSE=132
COLON=133 NULL=133
SEMI=134 ARROW=134
COMMA=135 COLON=135
DOT=136 SEMI=136
LBRACE=137 COMMA=137
RBRACE=138 DOT=138
LBRACK=139 LBRACE=139
RBRACK=140 RBRACE=140
LPAREN=141 LBRACK=141
RPAREN=142 RBRACK=142
LT=143 LPAREN=143
GT=144 RPAREN=144
AMP=145 LT=145
EQUAL=146 GT=146
INTEGER=147 AMP=147
JSON_NUMBER=148 EQUAL=148
IDENTIFIER=149 INTEGER=149
STRING_LITERAL=150 JSON_NUMBER=150
LINE_COMMENT=151 IDENTIFIER=151
BLOCK_COMMENT=152 STRING_LITERAL=152
WS=153 LINE_COMMENT=153
BLOCK_COMMENT=154
WS=155
'edit'=1 'edit'=1
'module'=2 'module'=2
'contract'=3 'contract'=3
@@ -158,142 +160,144 @@ WS=153
'subject'=5 'subject'=5
'for'=6 'for'=6
'creation'=7 'creation'=7
'initialize'=8 'variant'=8
'workspace'=9 'prepares'=9
'execution'=10 'initialize'=10
'snapshot'=11 'workspace'=11
'transaction'=12 'execution'=12
'remote'=13 'snapshot'=13
'targets'=14 'transaction'=14
'portable'=15 'remote'=15
'replica'=16 'targets'=16
'local'=17 'portable'=17
'from'=18 'replica'=18
'result-bytes'=19 'local'=19
'rpc-calls'=20 'from'=20
'deadline-ms'=21 'result-bytes'=21
'query'=22 'rpc-calls'=22
'reflection'=23 'deadline-ms'=23
'specialize'=24 'query'=24
'root'=25 'reflection'=25
'document'=26 'specialize'=26
'fragments'=27 'root'=27
'view'=28 'document'=28
'max'=29 'fragments'=29
'allow'=30 'view'=30
'poll'=31 'max'=31
'queryable'=32 'allow'=32
'rpc'=33 'poll'=33
'query-reason'=34 'queryable'=34
'type'=35 'rpc'=35
'object'=36 'query-reason'=36
'storable'=37 'type'=37
'implements'=38 'object'=38
'ref'=39 'storable'=39
'fragment'=40 'implements'=40
'import'=41 'ref'=41
'external'=42 'fragment'=42
'atom'=43 'import'=43
'interface'=44 'external'=44
'interfaces'=45 'atom'=45
'package'=46 'interface'=46
'value'=47 'interfaces'=47
'relation'=48 'package'=48
'operation'=49 'value'=49
'function'=50 'relation'=50
'constructor'=51 'operation'=51
'constructs'=52 'function'=52
'input'=53 'constructor'=53
'conform'=54 'constructs'=54
'as'=55 'input'=55
'bind'=56 'conform'=56
'static'=57 'as'=57
'to'=58 'bind'=58
'private'=59 'static'=59
'shared'=60 'to'=60
'state'=61 'private'=61
'edge'=62 'shared'=62
'projection'=63 'state'=63
'with'=64 'edge'=64
'using'=65 'projection'=65
'accept'=66 'with'=66
'via'=67 'using'=67
'materialize'=68 'accept'=68
'if'=69 'via'=69
'absent'=70 'materialize'=70
'on'=71 'if'=71
'policy'=72 'absent'=72
'default'=73 'on'=73
'source'=74 'policy'=74
'repository'=75 'default'=75
'commit'=76 'source'=76
'revision'=77 'repository'=77
'semantic-major'=78 'commit'=78
'on-delete'=79 'revision'=79
'lifecycle'=80 'semantic-major'=80
'retain-other'=81 'on-delete'=81
'keyed'=82 'lifecycle'=82
'public-traversal'=83 'retain-other'=83
'id'=84 'keyed'=84
'doc'=85 'public-traversal'=85
'mode'=86 'id'=86
'emits'=87 'doc'=87
'receiver'=88 'mode'=88
'requires'=89 'emits'=89
'any'=90 'receiver'=90
'get'=91 'requires'=91
'set'=92 'any'=92
'watch'=93 'get'=93
'start'=94 'set'=94
'stop'=95 'watch'=95
'read'=96 'start'=96
'write'=97 'stop'=97
'resolve'=98 'read'=98
'connect'=99 'write'=99
'disconnect'=100 'resolve'=100
'call'=101 'connect'=101
'watch-start'=102 'disconnect'=102
'watch-stop'=103 'call'=103
'subscribe'=104 'watch-start'=104
'unsubscribe'=105 'watch-stop'=105
'register'=106 'subscribe'=106
'crdt'=107 'unsubscribe'=107
'optional-one'=108 'register'=108
'exactly-one'=109 'crdt'=109
'many-unique'=110 'optional-one'=110
'many'=111 'exactly-one'=111
'ordered'=112 'many-unique'=112
'unit'=113 'many'=113
'watch-handle'=114 'ordered'=114
'message'=115 'unit'=115
'atom-ref'=116 'watch-handle'=116
'interface-ref'=117 'message'=117
'optional'=118 'atom-ref'=118
'list'=119 'interface-ref'=119
'record'=120 'optional'=120
'bool'=121 'list'=121
'bytes'=122 'record'=122
'double'=123 'bool'=123
'int32'=124 'bytes'=124
'int64'=125 'double'=125
'string'=126 'int32'=126
'uint32'=127 'int64'=127
'uint64'=128 'string'=128
'true'=129 'uint32'=129
'false'=130 'uint64'=130
'null'=131 'true'=131
'->'=132 'false'=132
':'=133 'null'=133
';'=134 '->'=134
','=135 ':'=135
'.'=136 ';'=136
'{'=137 ','=137
'}'=138 '.'=138
'['=139 '{'=139
']'=140 '}'=140
'('=141 '['=141
')'=142 ']'=142
'<'=143 '('=143
'>'=144 ')'=144
'&'=145 '<'=145
'='=146 '>'=146
'&'=147
'='=148
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -39,6 +39,7 @@ import { FragmentVariableContext } 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 { AcceptanceClauseContext } from "./QuixosCapabilityParser.js";
import { PreparationClauseContext } 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";
@@ -77,6 +78,7 @@ import { DependencyBindingContext } from "./QuixosCapabilityParser.js";
import { ConstructorBindingDeclContext } from "./QuixosCapabilityParser.js"; import { ConstructorBindingDeclContext } from "./QuixosCapabilityParser.js";
import { ValueTypeContext } from "./QuixosCapabilityParser.js"; import { ValueTypeContext } from "./QuixosCapabilityParser.js";
import { RecordFieldContext } from "./QuixosCapabilityParser.js"; import { RecordFieldContext } from "./QuixosCapabilityParser.js";
import { VariantCaseContext } from "./QuixosCapabilityParser.js";
import { ScalarTypeContext } from "./QuixosCapabilityParser.js"; import { ScalarTypeContext } from "./QuixosCapabilityParser.js";
import { CardinalityContext } from "./QuixosCapabilityParser.js"; import { CardinalityContext } from "./QuixosCapabilityParser.js";
import { JsonLiteralContext } from "./QuixosCapabilityParser.js"; import { JsonLiteralContext } from "./QuixosCapabilityParser.js";
@@ -317,6 +319,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitAcceptanceClause?: (ctx: AcceptanceClauseContext) => Result; visitAcceptanceClause?: (ctx: AcceptanceClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.preparationClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPreparationClause?: (ctx: PreparationClauseContext) => 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
@@ -545,6 +553,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result * @return the visitor result
*/ */
visitRecordField?: (ctx: RecordFieldContext) => Result; visitRecordField?: (ctx: RecordFieldContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.variantCase`.
* @param ctx the parse tree
* @return the visitor result
*/
visitVariantCase?: (ctx: VariantCaseContext) => Result;
/** /**
* Visit a parse tree produced by `QuixosCapabilityParser.scalarType`. * Visit a parse tree produced by `QuixosCapabilityParser.scalarType`.
* @param ctx the parse tree * @param ctx the parse tree
+19
View File
@@ -110,6 +110,7 @@ export class GenericSourceTypes {
} }
if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1); if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1);
if (type.kind === "record") return Object.values(type.fields).every((field) => storable(field, depth + 1)); if (type.kind === "record") return Object.values(type.fields).every((field) => storable(field, depth + 1));
if (type.kind === "variant") return Object.values(type.cases).every((entry) => storable(entry.value, depth + 1));
if (type.kind === "alias") { if (type.kind === "alias") {
const alias = aliases.get(type.definitionId); const alias = aliases.get(type.definitionId);
if (!alias || alias.parameters.length !== type.arguments.length) return false; if (!alias || alias.parameters.length !== type.arguments.length) return false;
@@ -301,6 +302,24 @@ export class GenericSourceTypes {
} }
expression(context: ValueTypeContext): ValueTypeExpression { expression(context: ValueTypeContext): ValueTypeExpression {
if (context.VARIANT()) {
const entries = context.variantCase().map(
(entry) =>
[
entry.identifier().getText(),
{
id: literal(entry.stringLiteral()),
value: this.expression(entry.valueType()),
},
] as const,
);
if (
new Set(entries.map(([label]) => label)).size !== entries.length ||
new Set(entries.map(([, entry]) => entry.id)).size !== entries.length
)
throw new GenericTypeError("duplicate-variant-case", "variant", "Variant labels and case IDs must be unique");
return { kind: "variant", id: literal(context.stringLiteral()!), cases: Object.fromEntries(entries) };
}
if (context.scalarType()) if (context.scalarType())
return { return {
kind: "scalar", kind: "scalar",
+21
View File
@@ -316,6 +316,7 @@ const lowerValueType = (state: LoweringState, context: ValueTypeContext | null):
throw new Error("Missing value type after a successful parse"); throw new Error("Missing value type after a successful parse");
} }
if ( if (
context.VARIANT() ||
context.REF() || context.REF() ||
context.typeArguments() || context.typeArguments() ||
(context.identifier() && !context.ATOM_REF() && !context.INTERFACE_REF()) (context.identifier() && !context.ATOM_REF() && !context.INTERFACE_REF())
@@ -1658,6 +1659,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.preparationClause())
loweringIssue(
state,
generic,
"unsupported-generic-preparation",
"Creation preparation currently requires a closed export",
);
if (generic.acceptanceClause()) if (generic.acceptanceClause())
loweringIssue( loweringIssue(
state, state,
@@ -1689,6 +1697,19 @@ const lowerPackage = (
// Resolve after all exports have been declared, so checks can follow bodies. // Resolve after all exports have been declared, so checks can follow bodies.
for (const source of context.packageExport()) { for (const source of context.packageExport()) {
const declaration = source.packageOperationExport() ?? source.packageFunctionExport(); const declaration = source.packageOperationExport() ?? source.packageFunctionExport();
const preparation = declaration?.preparationClause();
if (declaration && preparation && !declaration.typeParameters()) {
const entry = exports.find((entry) => entry.displayName === identifier(declaration.identifier()));
const port = entry?.dependencyPorts.find((port) => port.displayName === identifier(preparation.identifier()));
if (!entry || port?.requirement.kind !== "creation")
loweringIssue(
state,
preparation,
"invalid-creation-preparation",
"Preparation must name a creation port of this export",
);
else entry.preparation = { port: port.id, key: stringValue(preparation.stringLiteral()) };
}
const clause = declaration?.acceptanceClause(); const clause = declaration?.acceptanceClause();
if (!declaration || !clause || declaration.typeParameters()) continue; if (!declaration || !clause || declaration.typeParameters()) continue;
const entry = exports.find((entry) => entry.displayName === identifier(declaration.identifier())); const entry = exports.find((entry) => entry.displayName === identifier(declaration.identifier()));
+22
View File
@@ -16,6 +16,7 @@ export type ValueTypeExpression =
| Extract<ValueType, { kind: "builtin" | "scalar" | "message" }> | Extract<ValueType, { kind: "builtin" | "scalar" | "message" }>
| { kind: "parameter"; parameterId: string } | { kind: "parameter"; parameterId: string }
| { kind: "record"; fields: Record<string, ValueTypeExpression> } | { kind: "record"; fields: Record<string, ValueTypeExpression> }
| { kind: "variant"; id: string; cases: Record<string, { id: string; value: ValueTypeExpression }> }
| { kind: "list" | "optional" | "change"; value: ValueTypeExpression } | { kind: "list" | "optional" | "change"; value: ValueTypeExpression }
| { kind: "object-ref"; expectation: ObjectTypeExpression } | { kind: "object-ref"; expectation: ObjectTypeExpression }
| { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] }; | { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] };
@@ -195,6 +196,8 @@ export const isStorableType = (
return false; return false;
case "record": case "record":
return Object.values(value.fields).every((field) => visit(field, depth + 1)); return Object.values(value.fields).every((field) => visit(field, depth + 1));
case "variant":
return Object.values(value.cases).every((entry) => visit(entry.value, depth + 1));
} }
}; };
return visit(type, 0); return visit(type, 0);
@@ -221,6 +224,14 @@ export const canonicalTypeArgument = (argument: ClosedTypeArgument): string => {
return [value.kind, value.name]; return [value.kind, value.name];
case "message": case "message":
return ["message", value.descriptorId]; return ["message", value.descriptorId];
case "variant":
return [
"variant",
value.id,
Object.keys(value.cases)
.sort()
.map((label) => [label, value.cases[label].id, type(value.cases[label].value, depth + 1)]),
];
case "object-ref": case "object-ref":
return ["object-ref", target(value.expectation)]; return ["object-ref", target(value.expectation)];
case "change": case "change":
@@ -323,6 +334,17 @@ export class TypeSubstitution {
value(expression: ValueTypeExpression, path = "type", depth = 0): ValueType { value(expression: ValueTypeExpression, path = "type", depth = 0): ValueType {
this.budget.enter(path, depth); this.budget.enter(path, depth);
switch (expression.kind) { switch (expression.kind) {
case "variant":
return {
kind: "variant",
id: expression.id,
cases: Object.fromEntries(
Object.entries(expression.cases).map(([label, entry]) => [
label,
{ id: entry.id, value: this.value(entry.value, `${path}.${label}`, depth + 1) },
]),
),
};
case "builtin": case "builtin":
return { kind: "builtin", name: expression.name }; return { kind: "builtin", name: expression.name };
case "scalar": case "scalar":
+3
View File
@@ -65,6 +65,7 @@ export type ValueType =
| { kind: "scalar"; name: ScalarValueTypeName } | { kind: "scalar"; name: ScalarValueTypeName }
| { kind: "message"; descriptorId: string } | { kind: "message"; descriptorId: string }
| { kind: "record"; fields: Record<string, ValueType> } | { kind: "record"; fields: Record<string, ValueType> }
| { kind: "variant"; id: string; cases: Record<string, { id: string; value: ValueType }> }
| { kind: "object-ref"; expectation: ObjectExpectation } | { kind: "object-ref"; expectation: ObjectExpectation }
| { kind: "optional"; value: ValueType } | { kind: "optional"; value: ValueType }
| { kind: "list"; value: ValueType } | { kind: "list"; value: ValueType }
@@ -270,6 +271,8 @@ export interface DependencyPort {
} }
interface PackageExportBase { interface PackageExportBase {
/** Returns this declared birth, with unchanged operation input as its initializer. */
preparation?: { port: DependencyPortId; key: string };
/** Required authoritative snapshot export in this package. */ /** Required authoritative snapshot export in this package. */
acceptanceExportId?: PackageExportId; acceptanceExportId?: PackageExportId;
execution?: ExecutionContract; execution?: ExecutionContract;
+54
View File
@@ -148,6 +148,11 @@ const requireText = (issues: CapabilityValidationIssue[], value: string, path: s
const typeLabel = (type: ValueType): string => { const typeLabel = (type: ValueType): string => {
switch (type.kind) { switch (type.kind) {
case "variant":
return `variant:${type.id}{${Object.entries(type.cases)
.sort(([a], [b]) => a.localeCompare(b))
.map(([label, entry]) => `${label}:${entry.id}:${typeLabel(entry.value)}`)
.join(";")}}`;
case "builtin": case "builtin":
case "scalar": case "scalar":
return type.name; return type.name;
@@ -176,6 +181,19 @@ export const valueTypesEqual = (left: ValueType, right: ValueType): boolean => {
return false; return false;
} }
switch (left.kind) { switch (left.kind) {
case "variant": {
const other = right as typeof left;
return (
left.id === other.id &&
Object.keys(left.cases).length === Object.keys(other.cases).length &&
Object.entries(left.cases).every(
([label, entry]) =>
Object.hasOwn(other.cases, label) &&
entry.id === other.cases[label].id &&
valueTypesEqual(entry.value, other.cases[label].value),
)
);
}
case "builtin": case "builtin":
case "scalar": case "scalar":
return left.name === (right as typeof left).name; return left.name === (right as typeof left).name;
@@ -232,6 +250,20 @@ const validateValueType = (
indexes: Pick<ValidationIndexes, "atoms" | "interfaces">, indexes: Pick<ValidationIndexes, "atoms" | "interfaces">,
) => { ) => {
switch (type.kind) { switch (type.kind) {
case "variant": {
requireText(issues, type.id, `${path}.id`, "Variant identity");
const entries = Object.entries(type.cases),
ids = new Set<string>();
if (!entries.length || entries.length > 256) issue(issues, "invalid-value-type", path, "Use 1–256 variant cases");
for (const [label, entry] of entries) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(label)) issue(issues, "invalid-value-type", path, "Invalid variant label");
requireText(issues, entry.id, `${path}.${label}.id`, "Case identity");
if (ids.has(entry.id)) issue(issues, "invalid-value-type", path, "Duplicate case identity");
ids.add(entry.id);
validateValueType(issues, entry.value, `${path}.${label}`, indexes);
}
return;
}
case "record": case "record":
for (const [name, field] of Object.entries(type.fields)) { for (const [name, field] of Object.entries(type.fields)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
@@ -569,6 +601,7 @@ const collectIdentityIndexes = (
if (member.kind === "value") { if (member.kind === "value") {
const queryValue = (type: ValueType): boolean => const queryValue = (type: ValueType): boolean =>
type.kind === "scalar" || type.kind === "scalar" ||
(type.kind === "variant" && isStorableType(type)) ||
((type.kind === "optional" || type.kind === "list") && queryValue(type.value)) || ((type.kind === "optional" || type.kind === "list") && queryValue(type.value)) ||
(type.kind === "record" && (type.kind === "record" &&
Object.keys(type.fields).length > 0 && Object.keys(type.fields).length > 0 &&
@@ -893,6 +926,7 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
type.kind === "object-ref" || type.kind === "object-ref" ||
type.kind === "change" || type.kind === "change" ||
(type.kind === "record" && Object.values(type.fields).some(containsRpcType)) || (type.kind === "record" && Object.values(type.fields).some(containsRpcType)) ||
(type.kind === "variant" && Object.values(type.cases).some((entry) => containsRpcType(entry.value))) ||
((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value)); ((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value));
if ( if (
containsRpcType(attachment.valueType) || containsRpcType(attachment.valueType) ||
@@ -999,6 +1033,26 @@ 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.preparation) {
const port = entry.dependencyPorts.find((port) => port.id === entry.preparation!.port);
if (
entry.execution?.kind !== "transaction" ||
entry.kind === "constructor" ||
(entry.kind === "operation" && entry.mode !== "call") ||
!entry.preparation.key ||
port?.requirement.kind !== "creation" ||
!valueTypesEqual(port.requirement.inputType, entry.inputType) ||
entry.outputType.kind !== "object-ref" ||
(entry.outputType.expectation.kind === "atom" &&
entry.outputType.expectation.atomId !== port.requirement.atomId)
)
issue(
issues,
"invalid-dependency-binding",
exportPath,
"Prepared creation requires a transaction returning its declared creation, with matching captured input types and a fixed key",
);
}
if (entry.acceptanceExportId !== undefined) { if (entry.acceptanceExportId !== undefined) {
const check = revision.exports.find((candidate) => candidate.id === entry.acceptanceExportId); const check = revision.exports.find((candidate) => candidate.id === entry.acceptanceExportId);
if ( if (
+8 -1
View File
@@ -9,10 +9,15 @@ import type {
} 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";
function preparedInputPaths(type: ValueType, path: (string | null)[] = []): (string | null)[][] { type PreparedStep = string | null | { case: string; id: string };
function preparedInputPaths(type: ValueType, path: PreparedStep[] = []): PreparedStep[][] {
if (type.kind === "change") return [path]; if (type.kind === "change") return [path];
if (type.kind === "optional") return preparedInputPaths(type.value, path); if (type.kind === "optional") return preparedInputPaths(type.value, path);
if (type.kind === "list") return preparedInputPaths(type.value, [...path, null]); if (type.kind === "list") return preparedInputPaths(type.value, [...path, null]);
if (type.kind === "variant")
return Object.entries(type.cases).flatMap(([label, entry]) =>
preparedInputPaths(entry.value, [...path, { case: label, id: entry.id }]),
);
if (type.kind === "record") if (type.kind === "record")
return Object.entries(type.fields).flatMap(([key, value]) => preparedInputPaths(value, [...path, key])); return Object.entries(type.fields).flatMap(([key, value]) => preparedInputPaths(value, [...path, key]));
return []; return [];
@@ -210,6 +215,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
package: binding.packageRevisionId, package: binding.packageRevisionId,
export: binding.exportId, export: binding.exportId,
ports, ports,
...(entry.preparation ? { preparation: entry.preparation } : {}),
...(check ...(check
? { acceptance: { export: check.id, ports: compilePorts(check, binding.acceptanceDependencies ?? []) } } ? { acceptance: { export: check.id, ports: compilePorts(check, binding.acceptanceDependencies ?? []) } }
: {}), : {}),
@@ -237,6 +243,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.preparation ? { preparation: entry.preparation } : {}),
...(entry.acceptanceExportId ? { acceptance: entry.acceptanceExportId } : {}), ...(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 : [],
+3
View File
@@ -142,6 +142,9 @@ export function resolvePlannedOperation(
/** Bound argument/state codec subset. A parsed type alone does not mean a runtime implements it. */ /** Bound argument/state codec subset. A parsed type alone does not mean a runtime implements it. */
function checkValue(type: ValueType, path: string, allowChange = false): void { function checkValue(type: ValueType, path: string, allowChange = false): void {
switch (type.kind) { switch (type.kind) {
case "variant":
for (const [label, entry] of Object.entries(type.cases)) checkValue(entry.value, `${path}.${label}`, allowChange);
return;
case "scalar": case "scalar":
case "object-ref": case "object-ref":
return; return;
+21
View File
@@ -126,8 +126,29 @@ export function querySchema(
); );
return views[0]!.interfaceRevisionId; return views[0]!.interfaceRevisionId;
}; };
const variants = new Map<string, GraphQLScalarType>();
const scalar = (type: ValueType): GraphQLOutputType & GraphQLInputType => { const scalar = (type: ValueType): GraphQLOutputType & GraphQLInputType => {
if (type.kind === "optional") return scalar(type.value); if (type.kind === "optional") return scalar(type.value);
if (type.kind === "variant") {
const name = `QxVariant_${createHash("sha256").update(JSON.stringify(type)).digest("hex").slice(0, 16)}`;
let result = variants.get(name);
if (!result) {
result = new GraphQLScalarType({
name,
description: "Whole checked Camino variant value. Branch selection and predicates are not supported.",
serialize: (value) => value,
parseValue() {
throw new QueryCompileError("QUERY_TYPE", "Variant query inputs are not supported");
},
parseLiteral() {
throw new QueryCompileError("QUERY_TYPE", "Variant query inputs are not supported");
},
});
variants.set(name, result);
scalarTypes.set(name, type);
}
return result;
}
if (type.kind !== "scalar") if (type.kind !== "scalar")
throw new QueryCompileError("QUERY_TYPE", "Only scalar/optional scalar query fields are supported"); throw new QueryCompileError("QUERY_TYPE", "Only scalar/optional scalar query fields are supported");
return queryScalars[type.name]; return queryScalars[type.name];
+17
View File
@@ -154,3 +154,20 @@ await writeFile("dist/portable-reader/compound-crdt/kernel.json", JSON.stringify
const { emitExecutionScaffold } = await import("./execution-scaffold-fixture.js"); const { emitExecutionScaffold } = await import("./execution-scaffold-fixture.js");
const executionScaffold = await emitExecutionScaffold("dist/portable-reader/execution-scaffold/source"); const executionScaffold = await emitExecutionScaffold("dist/portable-reader/execution-scaffold/source");
await writeFile("dist/portable-reader/execution-scaffold/plan.json", JSON.stringify(executionScaffold)); await writeFile("dist/portable-reader/execution-scaffold/plan.json", JSON.stringify(executionScaffold));
// Native variant fixture shared by generated-type and browser editing qualification.
const { readFile } = await import("node:fs/promises");
const { compileCapabilityResourceSource } = await import("../src/capability-language/index.js");
const { portableValueSchema, tsType } = await import("../src/bindings/portable.js");
const variantSource = compileCapabilityResourceSource(await readFile("test/fixtures/canvas-content.qx", "utf8"), {
source: { repository: "https://example.test/canvas-content.git", commit: "a".repeat(40) },
});
if (!variantSource.ok || variantSource.resource.kind !== "interface") throw Error("Invalid variant fixture");
const entries = variantSource.resource.revision.members[0];
if (entries.kind !== "value") throw Error("Expected entries value");
await mkdir("dist/portable-reader/variants", { recursive: true });
await writeFile("dist/portable-reader/variants/schema.json", JSON.stringify(portableValueSchema(entries.valueType)));
await writeFile(
"dist/portable-reader/variants/entries.ts",
"export type Entries = " + tsType(entries.valueType) + ";\n",
);
+12
View File
@@ -0,0 +1,12 @@
type Point = record { x:double; y:double; };
type Entry = variant id "canvas:entry@1" {
paragraph id "canvas:paragraph" : record { text:string; };
stroke id "canvas:stroke" : record { points:list<Point>; };
image id "canvas:image" : record { asset:string; };
};
interface CanvasContent id "canvas" revision "canvas@1" {
queryable value entries id "entries" : list<Entry> {
get id "entries:get" execution snapshot targets [browser,server];
edit id "entries:edit" execution transaction targets [browser,server];
}
}
+82
View File
@@ -0,0 +1,82 @@
import { readFileSync } from "node:fs";
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
import { compileQuery } from "../src/query/compile.js";
import { defaultQueryBudgets } from "../src/query/types.js";
import { portableValueSchema, tsType } from "../src/bindings/portable.js";
import { isStorableType, TypeSubstitution } from "../src/capability-model/index.js";
const source = { repository: "https://example.test/variants.git", commit: "a".repeat(40) };
const declaration = readFileSync("test/fixtures/canvas-content.qx", "utf8");
const result = compileCapabilityResourceSource(declaration, { source });
assert.ok(result.ok, JSON.stringify(result.diagnostics));
if (result.resource.kind !== "interface") throw Error("Expected interface");
const iface = result.resource.revision;
const entries = iface.members[0];
if (entries.kind !== "value") throw Error("Expected value");
test("native variant aliases lower into nominal cases with typed payloads", () => {
assert.equal(entries.valueType.kind, "list");
assert.ok(isStorableType(entries.valueType));
const schema = portableValueSchema(entries.valueType) as any;
assert.equal(schema.value.id, "canvas:entry@1");
assert.equal(schema.value.cases.stroke.id, "canvas:stroke");
assert.match(tsType(entries.valueType), /kind: "stroke"/);
assert.match(tsType(entries.valueType), /ReadonlyArray<.*"x": number/);
const substitution = new TypeSubstitution({
arguments: new Map([["T", { kind: "value", type: { kind: "scalar", name: "string" } }]]),
applyInterface: () => {
throw Error("unused");
},
implementsInterface: () => false,
});
assert.deepEqual(
substitution.value({
kind: "variant",
id: "box",
cases: { some: { id: "box:some", value: { kind: "parameter", parameterId: "T" } } },
}),
{ kind: "variant", id: "box", cases: { some: { id: "box:some", value: { kind: "scalar", name: "string" } } } },
);
});
test("variant query scalars keep their checked payload type and reject branch selection", async () => {
const query = {
id: "canvas-query",
displayName: "Canvas",
root: { kind: "object" as const, interface: iface.revisionId },
document: "canvas.graphql",
operation: "Canvas",
fragments: [],
views: [],
allowances: [],
budgets: { ...defaultQueryBudgets },
watch: true,
};
const checked = await compileQuery(query, [iface], async () => `query Canvas { root { entries } }`);
assert.equal(checked.output.kind, "record");
if (checked.output.kind !== "record") return;
const root = checked.output.fields.root;
assert.equal(root.kind, "record");
if (root.kind !== "record") return;
assert.deepEqual(root.fields.entries, entries.valueType);
await assert.rejects(compileQuery(query, [iface], async () => `query Canvas { root { entries { kind value } } }`));
});
test("unknown and duplicate cases fail; reference payloads remain nonstorable", () => {
const duplicate = compileCapabilityResourceSource(declaration.replace('"canvas:image"', '"canvas:stroke"'), {
source,
});
assert.equal(duplicate.ok, false);
assert.ok(
!isStorableType({
kind: "variant",
id: "transport",
cases: {
ref: {
id: "ref",
value: { kind: "object-ref", expectation: { kind: "interface", interfaceRevisionId: iface.revisionId } },
},
},
}),
);
});