modality-ts 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/checker/encode/index.d.ts +2 -0
- package/dist/checker/encode/index.d.ts.map +1 -0
- package/dist/checker/encode/index.js +2 -0
- package/dist/checker/encode/index.js.map +1 -0
- package/dist/checker/index.d.ts +6 -0
- package/dist/checker/index.d.ts.map +1 -0
- package/dist/checker/index.js +6 -0
- package/dist/checker/index.js.map +1 -0
- package/dist/checker/monitors/index.d.ts +2 -0
- package/dist/checker/monitors/index.d.ts.map +1 -0
- package/dist/checker/monitors/index.js +2 -0
- package/dist/checker/monitors/index.js.map +1 -0
- package/dist/checker/search/eval.d.ts +16 -0
- package/dist/checker/search/eval.d.ts.map +1 -0
- package/dist/checker/search/eval.js +254 -0
- package/dist/checker/search/eval.js.map +1 -0
- package/dist/checker/search/index.d.ts +43 -0
- package/dist/checker/search/index.d.ts.map +1 -0
- package/dist/checker/search/index.js +532 -0
- package/dist/checker/search/index.js.map +1 -0
- package/dist/checker/slicing/index.d.ts +2 -0
- package/dist/checker/slicing/index.d.ts.map +1 -0
- package/dist/checker/slicing/index.js +2 -0
- package/dist/checker/slicing/index.js.map +1 -0
- package/dist/checker/traces/index.d.ts +2 -0
- package/dist/checker/traces/index.d.ts.map +1 -0
- package/dist/checker/traces/index.js +2 -0
- package/dist/checker/traces/index.js.map +1 -0
- package/dist/extraction/index.d.ts +31 -0
- package/dist/extraction/index.d.ts.map +1 -0
- package/dist/extraction/index.js +2254 -0
- package/dist/extraction/index.js.map +1 -0
- package/dist/extraction/pipeline/index.d.ts +45 -0
- package/dist/extraction/pipeline/index.d.ts.map +1 -0
- package/dist/extraction/pipeline/index.js +101 -0
- package/dist/extraction/pipeline/index.js.map +1 -0
- package/dist/extraction/spi/index.d.ts +99 -0
- package/dist/extraction/spi/index.d.ts.map +1 -0
- package/dist/extraction/spi/index.js +2 -0
- package/dist/extraction/spi/index.js.map +1 -0
- package/dist/harness/index.d.ts +109 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +377 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/kernel/artifacts/index.d.ts +11 -0
- package/dist/kernel/artifacts/index.d.ts.map +1 -0
- package/dist/kernel/artifacts/index.js +127 -0
- package/dist/kernel/artifacts/index.js.map +1 -0
- package/dist/kernel/index.d.ts +10 -0
- package/dist/kernel/index.d.ts.map +1 -0
- package/dist/kernel/index.js +10 -0
- package/dist/kernel/index.js.map +1 -0
- package/dist/kernel/ir/canonical.d.ts +5 -0
- package/dist/kernel/ir/canonical.d.ts.map +1 -0
- package/dist/kernel/ir/canonical.js +47 -0
- package/dist/kernel/ir/canonical.js.map +1 -0
- package/dist/kernel/ir/domains.d.ts +9 -0
- package/dist/kernel/ir/domains.d.ts.map +1 -0
- package/dist/kernel/ir/domains.js +102 -0
- package/dist/kernel/ir/domains.js.map +1 -0
- package/dist/kernel/ir/types.d.ts +224 -0
- package/dist/kernel/ir/types.d.ts.map +1 -0
- package/dist/kernel/ir/types.js +2 -0
- package/dist/kernel/ir/types.js.map +1 -0
- package/dist/kernel/ir/validator.d.ts +11 -0
- package/dist/kernel/ir/validator.d.ts.map +1 -0
- package/dist/kernel/ir/validator.js +674 -0
- package/dist/kernel/ir/validator.js.map +1 -0
- package/dist/kernel/overlay/index.d.ts +18 -0
- package/dist/kernel/overlay/index.d.ts.map +1 -0
- package/dist/kernel/overlay/index.js +59 -0
- package/dist/kernel/overlay/index.js.map +1 -0
- package/dist/kernel/props/index.d.ts +71 -0
- package/dist/kernel/props/index.d.ts.map +1 -0
- package/dist/kernel/props/index.js +122 -0
- package/dist/kernel/props/index.js.map +1 -0
- package/dist/kernel/report/types.d.ts +115 -0
- package/dist/kernel/report/types.d.ts.map +1 -0
- package/dist/kernel/report/types.js +2 -0
- package/dist/kernel/report/types.js.map +1 -0
- package/dist/kernel/trace/types.d.ts +19 -0
- package/dist/kernel/trace/types.d.ts.map +1 -0
- package/dist/kernel/trace/types.js +2 -0
- package/dist/kernel/trace/types.js.map +1 -0
- package/dist/modality/check.d.ts +3 -0
- package/dist/modality/check.d.ts.map +1 -0
- package/dist/modality/check.js +2 -0
- package/dist/modality/check.js.map +1 -0
- package/dist/modality/ci.d.ts +3 -0
- package/dist/modality/ci.d.ts.map +1 -0
- package/dist/modality/ci.js +2 -0
- package/dist/modality/ci.js.map +1 -0
- package/dist/modality/cli.d.ts +3 -0
- package/dist/modality/cli.d.ts.map +1 -0
- package/dist/modality/cli.js +223 -0
- package/dist/modality/cli.js.map +1 -0
- package/dist/modality/codegen/model.d.ts +3 -0
- package/dist/modality/codegen/model.d.ts.map +1 -0
- package/dist/modality/codegen/model.js +59 -0
- package/dist/modality/codegen/model.js.map +1 -0
- package/dist/modality/codegen/replay-test.d.ts +10 -0
- package/dist/modality/codegen/replay-test.d.ts.map +1 -0
- package/dist/modality/codegen/replay-test.js +104 -0
- package/dist/modality/codegen/replay-test.js.map +1 -0
- package/dist/modality/conform.d.ts +3 -0
- package/dist/modality/conform.d.ts.map +1 -0
- package/dist/modality/conform.js +2 -0
- package/dist/modality/conform.js.map +1 -0
- package/dist/modality/export-tla.d.ts +3 -0
- package/dist/modality/export-tla.d.ts.map +1 -0
- package/dist/modality/export-tla.js +2 -0
- package/dist/modality/export-tla.js.map +1 -0
- package/dist/modality/extract.d.ts +3 -0
- package/dist/modality/extract.d.ts.map +1 -0
- package/dist/modality/extract.js +2 -0
- package/dist/modality/extract.js.map +1 -0
- package/dist/modality/features/check/command.d.ts +23 -0
- package/dist/modality/features/check/command.d.ts.map +1 -0
- package/dist/modality/features/check/command.js +174 -0
- package/dist/modality/features/check/command.js.map +1 -0
- package/dist/modality/features/check/index.d.ts +3 -0
- package/dist/modality/features/check/index.d.ts.map +1 -0
- package/dist/modality/features/check/index.js +2 -0
- package/dist/modality/features/check/index.js.map +1 -0
- package/dist/modality/features/ci/command.d.ts +23 -0
- package/dist/modality/features/ci/command.d.ts.map +1 -0
- package/dist/modality/features/ci/command.js +176 -0
- package/dist/modality/features/ci/command.js.map +1 -0
- package/dist/modality/features/ci/index.d.ts +3 -0
- package/dist/modality/features/ci/index.d.ts.map +1 -0
- package/dist/modality/features/ci/index.js +2 -0
- package/dist/modality/features/ci/index.js.map +1 -0
- package/dist/modality/features/conform/command.d.ts +35 -0
- package/dist/modality/features/conform/command.d.ts.map +1 -0
- package/dist/modality/features/conform/command.js +162 -0
- package/dist/modality/features/conform/command.js.map +1 -0
- package/dist/modality/features/conform/index.d.ts +3 -0
- package/dist/modality/features/conform/index.d.ts.map +1 -0
- package/dist/modality/features/conform/index.js +2 -0
- package/dist/modality/features/conform/index.js.map +1 -0
- package/dist/modality/features/export/command.d.ts +13 -0
- package/dist/modality/features/export/command.d.ts.map +1 -0
- package/dist/modality/features/export/command.js +250 -0
- package/dist/modality/features/export/command.js.map +1 -0
- package/dist/modality/features/export/index.d.ts +3 -0
- package/dist/modality/features/export/index.d.ts.map +1 -0
- package/dist/modality/features/export/index.js +2 -0
- package/dist/modality/features/export/index.js.map +1 -0
- package/dist/modality/features/extract/command.d.ts +37 -0
- package/dist/modality/features/extract/command.d.ts.map +1 -0
- package/dist/modality/features/extract/command.js +443 -0
- package/dist/modality/features/extract/command.js.map +1 -0
- package/dist/modality/features/extract/index.d.ts +3 -0
- package/dist/modality/features/extract/index.d.ts.map +1 -0
- package/dist/modality/features/extract/index.js +2 -0
- package/dist/modality/features/extract/index.js.map +1 -0
- package/dist/modality/features/replay/command.d.ts +16 -0
- package/dist/modality/features/replay/command.d.ts.map +1 -0
- package/dist/modality/features/replay/command.js +50 -0
- package/dist/modality/features/replay/command.js.map +1 -0
- package/dist/modality/features/replay/index.d.ts +3 -0
- package/dist/modality/features/replay/index.d.ts.map +1 -0
- package/dist/modality/features/replay/index.js +2 -0
- package/dist/modality/features/replay/index.js.map +1 -0
- package/dist/modality/overlay.d.ts +3 -0
- package/dist/modality/overlay.d.ts.map +1 -0
- package/dist/modality/overlay.js +9 -0
- package/dist/modality/overlay.js.map +1 -0
- package/dist/modality/registry/index.d.ts +22 -0
- package/dist/modality/registry/index.d.ts.map +1 -0
- package/dist/modality/registry/index.js +89 -0
- package/dist/modality/registry/index.js.map +1 -0
- package/dist/modality/replay.d.ts +3 -0
- package/dist/modality/replay.d.ts.map +1 -0
- package/dist/modality/replay.js +2 -0
- package/dist/modality/replay.js.map +1 -0
- package/dist/runtime/index.d.ts +53 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +83 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/sources/jotai/harness.d.ts +13 -0
- package/dist/sources/jotai/harness.d.ts.map +1 -0
- package/dist/sources/jotai/harness.js +20 -0
- package/dist/sources/jotai/harness.js.map +1 -0
- package/dist/sources/jotai/index.d.ts +11 -0
- package/dist/sources/jotai/index.d.ts.map +1 -0
- package/dist/sources/jotai/index.js +405 -0
- package/dist/sources/jotai/index.js.map +1 -0
- package/dist/sources/router/harness.d.ts +9 -0
- package/dist/sources/router/harness.d.ts.map +1 -0
- package/dist/sources/router/harness.js +32 -0
- package/dist/sources/router/harness.js.map +1 -0
- package/dist/sources/router/index.d.ts +15 -0
- package/dist/sources/router/index.d.ts.map +1 -0
- package/dist/sources/router/index.js +41 -0
- package/dist/sources/router/index.js.map +1 -0
- package/dist/sources/swr/harness.d.ts +12 -0
- package/dist/sources/swr/harness.d.ts.map +1 -0
- package/dist/sources/swr/harness.js +25 -0
- package/dist/sources/swr/harness.js.map +1 -0
- package/dist/sources/swr/index.d.ts +51 -0
- package/dist/sources/swr/index.d.ts.map +1 -0
- package/dist/sources/swr/index.js +428 -0
- package/dist/sources/swr/index.js.map +1 -0
- package/dist/sources/use-state/harness.d.ts +12 -0
- package/dist/sources/use-state/harness.d.ts.map +1 -0
- package/dist/sources/use-state/harness.js +19 -0
- package/dist/sources/use-state/harness.js.map +1 -0
- package/dist/sources/use-state/index.d.ts +9 -0
- package/dist/sources/use-state/index.d.ts.map +1 -0
- package/dist/sources/use-state/index.js +248 -0
- package/dist/sources/use-state/index.js.map +1 -0
- package/package.json +136 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import { domainFingerprint, enumerateDomain, validateValue } from "./domains.js";
|
|
2
|
+
export function validateModel(model) {
|
|
3
|
+
const errors = [];
|
|
4
|
+
const varIds = new Set(model.vars.map((v) => v.id));
|
|
5
|
+
const varsById = new Map(model.vars.map((v) => [v.id, v]));
|
|
6
|
+
if (model.schemaVersion !== 1)
|
|
7
|
+
errors.push(`Unsupported model schemaVersion ${model.schemaVersion}`);
|
|
8
|
+
validateBounds(errors, model);
|
|
9
|
+
pushDuplicates(errors, "state var", model.vars.map((v) => v.id));
|
|
10
|
+
pushDuplicates(errors, "transition", model.transitions.map((t) => t.id));
|
|
11
|
+
for (const decl of model.vars)
|
|
12
|
+
validateDecl(errors, decl);
|
|
13
|
+
validateSystemVars(errors, varsById, model);
|
|
14
|
+
for (const transition of model.transitions)
|
|
15
|
+
validateTransition(errors, transition, varIds, varsById);
|
|
16
|
+
return { ok: errors.length === 0, errors };
|
|
17
|
+
}
|
|
18
|
+
export function effectReads(effect) {
|
|
19
|
+
const reads = new Set();
|
|
20
|
+
walkEffect(effect, (e) => {
|
|
21
|
+
for (const read of exprReadsInEffect(e))
|
|
22
|
+
reads.add(read);
|
|
23
|
+
});
|
|
24
|
+
return reads;
|
|
25
|
+
}
|
|
26
|
+
export function effectWrites(effect) {
|
|
27
|
+
const writes = new Set();
|
|
28
|
+
walkEffect(effect, (effectNode) => {
|
|
29
|
+
switch (effectNode.kind) {
|
|
30
|
+
case "assign":
|
|
31
|
+
case "havoc":
|
|
32
|
+
case "choose":
|
|
33
|
+
writes.add(effectNode.var);
|
|
34
|
+
break;
|
|
35
|
+
case "enqueue":
|
|
36
|
+
writes.add("sys:pending");
|
|
37
|
+
break;
|
|
38
|
+
case "dequeue":
|
|
39
|
+
writes.add("sys:pending");
|
|
40
|
+
break;
|
|
41
|
+
case "navigate":
|
|
42
|
+
writes.add("sys:route");
|
|
43
|
+
writes.add("sys:history");
|
|
44
|
+
break;
|
|
45
|
+
case "opaque":
|
|
46
|
+
for (const write of effectNode.ref.declaredWrites)
|
|
47
|
+
writes.add(write);
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return writes;
|
|
54
|
+
}
|
|
55
|
+
export function exprReads(expr) {
|
|
56
|
+
const reads = new Set();
|
|
57
|
+
walkExpr(expr, (node) => {
|
|
58
|
+
if (node.kind === "read")
|
|
59
|
+
reads.add(node.var);
|
|
60
|
+
});
|
|
61
|
+
return reads;
|
|
62
|
+
}
|
|
63
|
+
function validateBounds(errors, model) {
|
|
64
|
+
const bounds = model.bounds;
|
|
65
|
+
if (!Number.isInteger(bounds.maxDepth) || bounds.maxDepth < 0) {
|
|
66
|
+
errors.push("bounds.maxDepth must be a non-negative integer");
|
|
67
|
+
}
|
|
68
|
+
if (!Number.isInteger(bounds.maxPending) || bounds.maxPending < 0) {
|
|
69
|
+
errors.push("bounds.maxPending must be a non-negative integer");
|
|
70
|
+
}
|
|
71
|
+
if (!Number.isInteger(bounds.maxInternalSteps) || bounds.maxInternalSteps < 1) {
|
|
72
|
+
errors.push("bounds.maxInternalSteps must be a positive integer");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function validateSystemVars(errors, varsById, model) {
|
|
76
|
+
const route = varsById.get("sys:route");
|
|
77
|
+
const history = varsById.get("sys:history");
|
|
78
|
+
const pending = varsById.get("sys:pending");
|
|
79
|
+
validateSystemDecl(errors, "sys:route", route);
|
|
80
|
+
validateSystemDecl(errors, "sys:history", history);
|
|
81
|
+
validateSystemDecl(errors, "sys:pending", pending);
|
|
82
|
+
if (route && route.domain.kind !== "enum") {
|
|
83
|
+
errors.push("sys:route must use an enum domain");
|
|
84
|
+
}
|
|
85
|
+
if (history) {
|
|
86
|
+
if (history.domain.kind !== "boundedList") {
|
|
87
|
+
errors.push("sys:history must use a boundedList domain");
|
|
88
|
+
}
|
|
89
|
+
else if (route && domainFingerprint(history.domain.inner) !== domainFingerprint(route.domain)) {
|
|
90
|
+
errors.push("sys:history inner domain must match sys:route domain");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (pending) {
|
|
94
|
+
if (pending.domain.kind !== "boundedList") {
|
|
95
|
+
errors.push("sys:pending must use a boundedList domain");
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
if (pending.domain.maxLen !== model.bounds.maxPending) {
|
|
99
|
+
errors.push("sys:pending maxLen must match bounds.maxPending");
|
|
100
|
+
}
|
|
101
|
+
validatePendingOpDomain(errors, pending.domain.inner);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function validateSystemDecl(errors, id, decl) {
|
|
106
|
+
if (!decl) {
|
|
107
|
+
errors.push(`Missing required system var ${id}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (decl.origin !== "system")
|
|
111
|
+
errors.push(`${id} must have system origin`);
|
|
112
|
+
if (decl.scope.kind !== "global")
|
|
113
|
+
errors.push(`${id} must have global scope`);
|
|
114
|
+
}
|
|
115
|
+
function validatePendingOpDomain(errors, domain) {
|
|
116
|
+
if (domain.kind !== "record") {
|
|
117
|
+
errors.push("sys:pending items must use a record domain");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const { opId, continuation, args } = domain.fields;
|
|
121
|
+
if (!opId)
|
|
122
|
+
errors.push("sys:pending item domain missing opId");
|
|
123
|
+
else if (opId.kind !== "enum")
|
|
124
|
+
errors.push("sys:pending opId must use an enum domain");
|
|
125
|
+
if (!continuation)
|
|
126
|
+
errors.push("sys:pending item domain missing continuation");
|
|
127
|
+
else if (continuation.kind !== "enum")
|
|
128
|
+
errors.push("sys:pending continuation must use an enum domain");
|
|
129
|
+
if (!args)
|
|
130
|
+
errors.push("sys:pending item domain missing args");
|
|
131
|
+
else if (args.kind !== "record")
|
|
132
|
+
errors.push("sys:pending args must use a record domain");
|
|
133
|
+
}
|
|
134
|
+
function validateDecl(errors, decl) {
|
|
135
|
+
const beforeDomainValidation = errors.length;
|
|
136
|
+
validateDomainShape(errors, decl.id, decl.domain);
|
|
137
|
+
if (errors.length > beforeDomainValidation)
|
|
138
|
+
return;
|
|
139
|
+
const initials = initialValues(decl.domain, decl.initial);
|
|
140
|
+
if (initials.length === 0)
|
|
141
|
+
errors.push(`${decl.id}: initial must not be empty`);
|
|
142
|
+
for (const value of initials) {
|
|
143
|
+
if (!validateValue(decl.domain, value)) {
|
|
144
|
+
errors.push(`${decl.id}: invalid initial ${JSON.stringify(value)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
enumerateDomain(decl.domain);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
errors.push(`${decl.id}: domain cannot enumerate: ${error.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function validateDomainShape(errors, owner, domain) {
|
|
155
|
+
switch (domain.kind) {
|
|
156
|
+
case "bool":
|
|
157
|
+
case "lengthCat":
|
|
158
|
+
return;
|
|
159
|
+
case "enum":
|
|
160
|
+
if (!Array.isArray(domain.values) || domain.values.length === 0) {
|
|
161
|
+
errors.push(`${owner}: enum domain must have at least one value`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
if (!domain.values.every((value) => typeof value === "string"))
|
|
165
|
+
errors.push(`${owner}: enum values must be strings`);
|
|
166
|
+
pushDuplicateDomainValues(errors, owner, "enum value", domain.values);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
case "boundedInt":
|
|
170
|
+
if (!Number.isInteger(domain.min) || !Number.isInteger(domain.max))
|
|
171
|
+
errors.push(`${owner}: boundedInt min/max must be integers`);
|
|
172
|
+
else if (domain.min > domain.max)
|
|
173
|
+
errors.push(`${owner}: boundedInt min must be <= max`);
|
|
174
|
+
return;
|
|
175
|
+
case "option":
|
|
176
|
+
validateDomainShape(errors, `${owner}.inner`, domain.inner);
|
|
177
|
+
return;
|
|
178
|
+
case "record":
|
|
179
|
+
for (const [field, fieldDomain] of Object.entries(domain.fields))
|
|
180
|
+
validateDomainShape(errors, `${owner}.${field}`, fieldDomain);
|
|
181
|
+
return;
|
|
182
|
+
case "tagged":
|
|
183
|
+
if (!domain.tag)
|
|
184
|
+
errors.push(`${owner}: tagged domain must have a tag field`);
|
|
185
|
+
if (Object.keys(domain.variants).length === 0)
|
|
186
|
+
errors.push(`${owner}: tagged domain must have at least one variant`);
|
|
187
|
+
for (const [variant, variantDomain] of Object.entries(domain.variants)) {
|
|
188
|
+
if (variantDomain.kind !== "record")
|
|
189
|
+
errors.push(`${owner}: tagged variant ${variant} must be a record domain`);
|
|
190
|
+
validateDomainShape(errors, `${owner}.${variant}`, variantDomain);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
case "tokens":
|
|
194
|
+
if (!Number.isInteger(domain.count) || domain.count < 1)
|
|
195
|
+
errors.push(`${owner}: tokens count must be a positive integer`);
|
|
196
|
+
if (domain.names) {
|
|
197
|
+
if (domain.names.length !== domain.count)
|
|
198
|
+
errors.push(`${owner}: tokens names length must match count`);
|
|
199
|
+
if (!domain.names.every((value) => typeof value === "string" && value.length > 0))
|
|
200
|
+
errors.push(`${owner}: token names must be non-empty strings`);
|
|
201
|
+
pushDuplicateDomainValues(errors, owner, "token name", domain.names);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
case "boundedList":
|
|
205
|
+
if (!Number.isInteger(domain.maxLen) || domain.maxLen < 0)
|
|
206
|
+
errors.push(`${owner}: boundedList maxLen must be a non-negative integer`);
|
|
207
|
+
validateDomainShape(errors, `${owner}.inner`, domain.inner);
|
|
208
|
+
return;
|
|
209
|
+
default:
|
|
210
|
+
errors.push(`${owner}: unknown domain kind ${domain.kind}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function pushDuplicateDomainValues(errors, owner, kind, values) {
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
for (const value of values) {
|
|
216
|
+
if (seen.has(value))
|
|
217
|
+
errors.push(`${owner}: duplicate ${kind} ${value}`);
|
|
218
|
+
seen.add(value);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export function initialValues(domain, initial) {
|
|
222
|
+
return domain.kind === "boundedList" ? [initial] : Array.isArray(initial) ? initial : [initial];
|
|
223
|
+
}
|
|
224
|
+
function validateTransition(errors, transition, varIds, varsById) {
|
|
225
|
+
const declaredReads = new Set(transition.reads);
|
|
226
|
+
const declaredWrites = new Set(transition.writes);
|
|
227
|
+
for (const id of [...transition.reads, ...transition.writes]) {
|
|
228
|
+
if (!varIds.has(id))
|
|
229
|
+
errors.push(`${transition.id}: references unknown var ${id}`);
|
|
230
|
+
}
|
|
231
|
+
validateTriggeredBy(errors, transition, varIds);
|
|
232
|
+
for (const read of exprReads(transition.guard)) {
|
|
233
|
+
if (!declaredReads.has(read))
|
|
234
|
+
errors.push(`${transition.id}: guard reads ${read} but reads does not declare it`);
|
|
235
|
+
}
|
|
236
|
+
for (const read of effectReads(transition.effect)) {
|
|
237
|
+
if (!declaredReads.has(read))
|
|
238
|
+
errors.push(`${transition.id}: effect reads ${read} but reads does not declare it`);
|
|
239
|
+
}
|
|
240
|
+
const actualWrites = effectWrites(transition.effect);
|
|
241
|
+
for (const write of actualWrites) {
|
|
242
|
+
if (!declaredWrites.has(write))
|
|
243
|
+
errors.push(`${transition.id}: effect writes ${write} but writes does not declare it`);
|
|
244
|
+
}
|
|
245
|
+
validateRouteLocalWrites(errors, transition, actualWrites, varsById);
|
|
246
|
+
validateExprShape(errors, transition.id, transition.guard);
|
|
247
|
+
validateEffectShape(errors, transition.id, transition.effect);
|
|
248
|
+
validateExprReferences(errors, transition.id, transition.guard, varsById);
|
|
249
|
+
walkEffect(transition.effect, (effectNode) => {
|
|
250
|
+
for (const expr of effectExpressions(effectNode))
|
|
251
|
+
validateExprReferences(errors, transition.id, expr, varsById);
|
|
252
|
+
});
|
|
253
|
+
validateExprType(errors, transition.id, transition.guard, varsById, "guard");
|
|
254
|
+
validateEffectTypes(errors, transition.id, transition.effect, varsById);
|
|
255
|
+
validateEffectValues(errors, transition.id, transition.effect, varsById);
|
|
256
|
+
}
|
|
257
|
+
function validateTriggeredBy(errors, transition, varIds) {
|
|
258
|
+
if (!transition.triggeredBy || transition.triggeredBy.length === 0)
|
|
259
|
+
return;
|
|
260
|
+
if (transition.cls !== "internal") {
|
|
261
|
+
errors.push(`${transition.id}: triggeredBy is only valid on internal transitions`);
|
|
262
|
+
}
|
|
263
|
+
for (const id of transition.triggeredBy) {
|
|
264
|
+
if (!varIds.has(id))
|
|
265
|
+
errors.push(`${transition.id}: triggeredBy references unknown var ${id}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function validateRouteLocalWrites(errors, transition, actualWrites, varsById) {
|
|
269
|
+
const routeLocalWrites = [...actualWrites]
|
|
270
|
+
.map((id) => varsById.get(id))
|
|
271
|
+
.filter((decl) => decl?.scope.kind === "route-local");
|
|
272
|
+
if (routeLocalWrites.length === 0)
|
|
273
|
+
return;
|
|
274
|
+
const routes = [...new Set(routeLocalWrites.map((decl) => decl.scope.route))].sort();
|
|
275
|
+
if (routes.length > 1)
|
|
276
|
+
errors.push(`${transition.id}: writes route-local vars for multiple routes: ${routes.join(", ")}`);
|
|
277
|
+
if (actualWrites.has("sys:route") || actualWrites.has("sys:history")) {
|
|
278
|
+
errors.push(`${transition.id}: writes route-local vars while navigating`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function validateEffectShape(errors, transitionId, effect) {
|
|
282
|
+
walkEffect(effect, (node) => {
|
|
283
|
+
switch (node.kind) {
|
|
284
|
+
case "choose":
|
|
285
|
+
if (!Array.isArray(node.among) || node.among.length === 0)
|
|
286
|
+
errors.push(`${transitionId}: choose must have at least one option`);
|
|
287
|
+
for (const expr of node.among)
|
|
288
|
+
validateExprShape(errors, transitionId, expr);
|
|
289
|
+
break;
|
|
290
|
+
case "seq":
|
|
291
|
+
if (!Array.isArray(node.effects))
|
|
292
|
+
errors.push(`${transitionId}: seq effects must be an array`);
|
|
293
|
+
break;
|
|
294
|
+
case "if":
|
|
295
|
+
validateExprShape(errors, transitionId, node.cond);
|
|
296
|
+
break;
|
|
297
|
+
case "assign":
|
|
298
|
+
validateExprShape(errors, transitionId, node.expr);
|
|
299
|
+
break;
|
|
300
|
+
case "enqueue":
|
|
301
|
+
for (const expr of Object.values(node.args))
|
|
302
|
+
validateExprShape(errors, transitionId, expr);
|
|
303
|
+
break;
|
|
304
|
+
case "navigate":
|
|
305
|
+
if (node.to)
|
|
306
|
+
validateExprShape(errors, transitionId, node.to);
|
|
307
|
+
break;
|
|
308
|
+
case "dequeue":
|
|
309
|
+
if (!Number.isInteger(node.index) || node.index < 0)
|
|
310
|
+
errors.push(`${transitionId}: dequeue index must be a non-negative integer`);
|
|
311
|
+
break;
|
|
312
|
+
default:
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function validateExprShape(errors, transitionId, expr) {
|
|
318
|
+
walkExpr(expr, (node) => {
|
|
319
|
+
switch (node.kind) {
|
|
320
|
+
case "eq":
|
|
321
|
+
case "neq":
|
|
322
|
+
if (!Array.isArray(node.args) || node.args.length !== 2)
|
|
323
|
+
errors.push(`${transitionId}: ${node.kind} expression must have exactly 2 args`);
|
|
324
|
+
break;
|
|
325
|
+
case "and":
|
|
326
|
+
case "or":
|
|
327
|
+
if (!Array.isArray(node.args) || node.args.length === 0)
|
|
328
|
+
errors.push(`${transitionId}: ${node.kind} expression must have at least 1 arg`);
|
|
329
|
+
break;
|
|
330
|
+
case "not":
|
|
331
|
+
if (!Array.isArray(node.args) || node.args.length !== 1)
|
|
332
|
+
errors.push(`${transitionId}: not expression must have exactly 1 arg`);
|
|
333
|
+
break;
|
|
334
|
+
case "cond":
|
|
335
|
+
if (!Array.isArray(node.args) || node.args.length !== 3)
|
|
336
|
+
errors.push(`${transitionId}: cond expression must have exactly 3 args`);
|
|
337
|
+
break;
|
|
338
|
+
case "updateField":
|
|
339
|
+
if (!Array.isArray(node.path) || node.path.length === 0)
|
|
340
|
+
errors.push(`${transitionId}: updateField path must not be empty`);
|
|
341
|
+
break;
|
|
342
|
+
default:
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function pushDuplicates(errors, kind, ids) {
|
|
348
|
+
const seen = new Set();
|
|
349
|
+
for (const id of ids) {
|
|
350
|
+
if (seen.has(id))
|
|
351
|
+
errors.push(`Duplicate ${kind} id ${id}`);
|
|
352
|
+
seen.add(id);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function exprReadsInEffect(effect) {
|
|
356
|
+
switch (effect.kind) {
|
|
357
|
+
case "assign":
|
|
358
|
+
return exprReads(effect.expr);
|
|
359
|
+
case "choose":
|
|
360
|
+
return union(effect.among.map(exprReads));
|
|
361
|
+
case "if":
|
|
362
|
+
return exprReads(effect.cond);
|
|
363
|
+
case "enqueue":
|
|
364
|
+
return union(Object.values(effect.args).map(exprReads));
|
|
365
|
+
case "navigate":
|
|
366
|
+
return effect.to ? exprReads(effect.to) : new Set();
|
|
367
|
+
case "opaque":
|
|
368
|
+
return new Set(effect.ref.declaredReads);
|
|
369
|
+
default:
|
|
370
|
+
return new Set();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function walkEffect(effect, visit) {
|
|
374
|
+
visit(effect);
|
|
375
|
+
if (effect.kind === "seq" && Array.isArray(effect.effects))
|
|
376
|
+
effect.effects.forEach((e) => walkEffect(e, visit));
|
|
377
|
+
if (effect.kind === "if") {
|
|
378
|
+
walkEffect(effect.then, visit);
|
|
379
|
+
walkEffect(effect.else, visit);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function walkExpr(expr, visit) {
|
|
383
|
+
visit(expr);
|
|
384
|
+
switch (expr.kind) {
|
|
385
|
+
case "eq":
|
|
386
|
+
case "neq":
|
|
387
|
+
case "and":
|
|
388
|
+
case "or":
|
|
389
|
+
if (Array.isArray(expr.args))
|
|
390
|
+
expr.args.forEach((arg) => walkExpr(arg, visit));
|
|
391
|
+
break;
|
|
392
|
+
case "not":
|
|
393
|
+
if (Array.isArray(expr.args) && expr.args[0])
|
|
394
|
+
walkExpr(expr.args[0], visit);
|
|
395
|
+
break;
|
|
396
|
+
case "cond":
|
|
397
|
+
if (Array.isArray(expr.args))
|
|
398
|
+
expr.args.forEach((arg) => walkExpr(arg, visit));
|
|
399
|
+
break;
|
|
400
|
+
case "updateField":
|
|
401
|
+
walkExpr(expr.target, visit);
|
|
402
|
+
walkExpr(expr.value, visit);
|
|
403
|
+
break;
|
|
404
|
+
case "tagIs":
|
|
405
|
+
case "lenCat":
|
|
406
|
+
walkExpr(expr.arg, visit);
|
|
407
|
+
break;
|
|
408
|
+
default:
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function validateEffectValues(errors, transitionId, effect, varsById) {
|
|
413
|
+
walkEffect(effect, (effectNode) => {
|
|
414
|
+
switch (effectNode.kind) {
|
|
415
|
+
case "assign":
|
|
416
|
+
validateAssignedExpr(errors, transitionId, effectNode.var, effectNode.expr, varsById);
|
|
417
|
+
break;
|
|
418
|
+
case "choose":
|
|
419
|
+
for (const expr of effectNode.among)
|
|
420
|
+
validateAssignedExpr(errors, transitionId, effectNode.var, expr, varsById);
|
|
421
|
+
break;
|
|
422
|
+
case "havoc":
|
|
423
|
+
if (!varsById.has(effectNode.var))
|
|
424
|
+
errors.push(`${transitionId}: havoc targets unknown var ${effectNode.var}`);
|
|
425
|
+
break;
|
|
426
|
+
default:
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
function validateAssignedExpr(errors, transitionId, varId, expr, varsById) {
|
|
432
|
+
const decl = varsById.get(varId);
|
|
433
|
+
if (!decl) {
|
|
434
|
+
errors.push(`${transitionId}: assignment targets unknown var ${varId}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (expr.kind === "lit" && !validateValue(decl.domain, expr.value)) {
|
|
438
|
+
errors.push(`${transitionId}: invalid assignment to ${varId}: ${JSON.stringify(expr.value)}`);
|
|
439
|
+
}
|
|
440
|
+
const exprDomain = inferExprDomain(errors, transitionId, expr, varsById);
|
|
441
|
+
if (exprDomain && !sameDomain(exprDomain, decl.domain)) {
|
|
442
|
+
errors.push(`${transitionId}: assignment to ${varId} expects ${domainFingerprint(decl.domain)} but got ${domainFingerprint(exprDomain)}`);
|
|
443
|
+
}
|
|
444
|
+
if (expr.kind === "freshToken" && decl.domain.kind !== "tokens") {
|
|
445
|
+
errors.push(`${transitionId}: freshToken assignment to ${varId} requires a tokens target`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function validateEffectTypes(errors, transitionId, effect, varsById) {
|
|
449
|
+
walkEffect(effect, (effectNode) => {
|
|
450
|
+
if (effectNode.kind === "if")
|
|
451
|
+
validateExprType(errors, transitionId, effectNode.cond, varsById, "if condition");
|
|
452
|
+
if (effectNode.kind === "navigate" && effectNode.to) {
|
|
453
|
+
const route = varsById.get("sys:route");
|
|
454
|
+
const toDomain = inferExprDomain(errors, transitionId, effectNode.to, varsById);
|
|
455
|
+
if (route && toDomain && !sameDomain(toDomain, route.domain)) {
|
|
456
|
+
errors.push(`${transitionId}: navigate target expects ${domainFingerprint(route.domain)} but got ${domainFingerprint(toDomain)}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function validateExprType(errors, transitionId, expr, varsById, context) {
|
|
462
|
+
if (expr.kind === "lit" && typeof expr.value !== "boolean") {
|
|
463
|
+
errors.push(`${transitionId}: ${context} must be boolean but got literal ${JSON.stringify(expr.value)}`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const domain = inferExprDomain(errors, transitionId, expr, varsById);
|
|
467
|
+
if (domain && !isBoolDomain(domain))
|
|
468
|
+
errors.push(`${transitionId}: ${context} must be boolean but got ${domainFingerprint(domain)}`);
|
|
469
|
+
}
|
|
470
|
+
function inferExprDomain(errors, transitionId, expr, varsById) {
|
|
471
|
+
switch (expr.kind) {
|
|
472
|
+
case "lit":
|
|
473
|
+
return inferLiteralDomain(expr.value);
|
|
474
|
+
case "read": {
|
|
475
|
+
const decl = varsById.get(expr.var);
|
|
476
|
+
return decl ? domainAtPath(decl.domain, expr.path ?? []) : undefined;
|
|
477
|
+
}
|
|
478
|
+
case "freshToken": {
|
|
479
|
+
const decl = varsById.get(expr.domainOf);
|
|
480
|
+
return decl?.domain.kind === "tokens" ? decl.domain : undefined;
|
|
481
|
+
}
|
|
482
|
+
case "eq":
|
|
483
|
+
case "neq":
|
|
484
|
+
if (Array.isArray(expr.args) && expr.args.length === 2) {
|
|
485
|
+
const left = inferExprDomain(errors, transitionId, expr.args[0], varsById);
|
|
486
|
+
const right = inferExprDomain(errors, transitionId, expr.args[1], varsById);
|
|
487
|
+
if (left && right && !sameDomain(left, right)) {
|
|
488
|
+
errors.push(`${transitionId}: ${expr.kind} compares ${domainFingerprint(left)} with ${domainFingerprint(right)}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return bool;
|
|
492
|
+
case "and":
|
|
493
|
+
case "or":
|
|
494
|
+
if (Array.isArray(expr.args)) {
|
|
495
|
+
for (const arg of expr.args)
|
|
496
|
+
validateBooleanOperand(errors, transitionId, expr.kind, arg, varsById);
|
|
497
|
+
}
|
|
498
|
+
return bool;
|
|
499
|
+
case "not":
|
|
500
|
+
if (Array.isArray(expr.args) && expr.args[0])
|
|
501
|
+
validateBooleanOperand(errors, transitionId, "not", expr.args[0], varsById);
|
|
502
|
+
return bool;
|
|
503
|
+
case "cond":
|
|
504
|
+
return inferCondDomain(errors, transitionId, expr, varsById);
|
|
505
|
+
case "updateField":
|
|
506
|
+
return inferUpdateFieldDomain(errors, transitionId, expr, varsById);
|
|
507
|
+
case "tagIs": {
|
|
508
|
+
const argDomain = inferExprDomain(errors, transitionId, expr.arg, varsById);
|
|
509
|
+
if (argDomain && argDomain.kind !== "tagged") {
|
|
510
|
+
errors.push(`${transitionId}: tagIs expects tagged argument but got ${domainFingerprint(argDomain)}`);
|
|
511
|
+
}
|
|
512
|
+
return bool;
|
|
513
|
+
}
|
|
514
|
+
case "lenCat": {
|
|
515
|
+
const argDomain = inferExprDomain(errors, transitionId, expr.arg, varsById);
|
|
516
|
+
if (argDomain && argDomain.kind !== "boundedList") {
|
|
517
|
+
errors.push(`${transitionId}: lenCat expects boundedList argument but got ${domainFingerprint(argDomain)}`);
|
|
518
|
+
}
|
|
519
|
+
return { kind: "lengthCat" };
|
|
520
|
+
}
|
|
521
|
+
default:
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const bool = { kind: "bool" };
|
|
526
|
+
function inferLiteralDomain(value) {
|
|
527
|
+
if (typeof value === "boolean")
|
|
528
|
+
return bool;
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
function inferCondDomain(errors, transitionId, expr, varsById) {
|
|
532
|
+
if (!Array.isArray(expr.args) || expr.args.length !== 3)
|
|
533
|
+
return undefined;
|
|
534
|
+
validateBooleanOperand(errors, transitionId, "cond condition", expr.args[0], varsById);
|
|
535
|
+
const thenDomain = inferExprDomain(errors, transitionId, expr.args[1], varsById);
|
|
536
|
+
const elseDomain = inferExprDomain(errors, transitionId, expr.args[2], varsById);
|
|
537
|
+
if (isNullLiteral(expr.args[1]) && elseDomain)
|
|
538
|
+
return { kind: "option", inner: elseDomain };
|
|
539
|
+
if (isNullLiteral(expr.args[2]) && thenDomain)
|
|
540
|
+
return { kind: "option", inner: thenDomain };
|
|
541
|
+
if (thenDomain && elseDomain && !sameDomain(thenDomain, elseDomain)) {
|
|
542
|
+
errors.push(`${transitionId}: cond branches have incompatible domains ${domainFingerprint(thenDomain)} and ${domainFingerprint(elseDomain)}`);
|
|
543
|
+
}
|
|
544
|
+
return thenDomain && elseDomain && sameDomain(thenDomain, elseDomain) ? thenDomain : thenDomain ?? elseDomain;
|
|
545
|
+
}
|
|
546
|
+
function isNullLiteral(expr) {
|
|
547
|
+
return expr.kind === "lit" && expr.value === null;
|
|
548
|
+
}
|
|
549
|
+
function inferUpdateFieldDomain(errors, transitionId, expr, varsById) {
|
|
550
|
+
const targetDomain = inferExprDomain(errors, transitionId, expr.target, varsById);
|
|
551
|
+
if (!targetDomain || !Array.isArray(expr.path) || expr.path.length === 0)
|
|
552
|
+
return targetDomain;
|
|
553
|
+
const fieldDomain = domainAtPath(targetDomain, expr.path);
|
|
554
|
+
if (!fieldDomain) {
|
|
555
|
+
errors.push(`${transitionId}: updateField has invalid path ${expr.path.join(".")} for ${domainFingerprint(targetDomain)}`);
|
|
556
|
+
return targetDomain;
|
|
557
|
+
}
|
|
558
|
+
const valueDomain = inferExprDomain(errors, transitionId, expr.value, varsById);
|
|
559
|
+
if (valueDomain && !sameDomain(valueDomain, fieldDomain)) {
|
|
560
|
+
errors.push(`${transitionId}: updateField ${expr.path.join(".")} expects ${domainFingerprint(fieldDomain)} but got ${domainFingerprint(valueDomain)}`);
|
|
561
|
+
}
|
|
562
|
+
return targetDomain;
|
|
563
|
+
}
|
|
564
|
+
function validateBooleanOperand(errors, transitionId, context, expr, varsById) {
|
|
565
|
+
const domain = inferExprDomain(errors, transitionId, expr, varsById);
|
|
566
|
+
if (domain && !isBoolDomain(domain))
|
|
567
|
+
errors.push(`${transitionId}: ${context} expects boolean operand but got ${domainFingerprint(domain)}`);
|
|
568
|
+
}
|
|
569
|
+
function isBoolDomain(domain) {
|
|
570
|
+
return domain.kind === "bool";
|
|
571
|
+
}
|
|
572
|
+
function sameDomain(left, right) {
|
|
573
|
+
return domainFingerprint(left) === domainFingerprint(right);
|
|
574
|
+
}
|
|
575
|
+
function validateExprReferences(errors, transitionId, expr, varsById) {
|
|
576
|
+
walkExpr(expr, (node) => {
|
|
577
|
+
if (node.kind === "read") {
|
|
578
|
+
validateReadReference(errors, transitionId, node, varsById);
|
|
579
|
+
}
|
|
580
|
+
if ((node.kind === "eq" || node.kind === "neq") && node.args.length === 2) {
|
|
581
|
+
validateReadLiteralComparison(errors, transitionId, node.args[0], node.args[1], varsById);
|
|
582
|
+
validateReadLiteralComparison(errors, transitionId, node.args[1], node.args[0], varsById);
|
|
583
|
+
}
|
|
584
|
+
if (node.kind === "tagIs" && node.arg.kind === "read") {
|
|
585
|
+
const decl = varsById.get(node.arg.var);
|
|
586
|
+
if (decl?.domain.kind === "tagged" && !Object.hasOwn(decl.domain.variants, node.tag)) {
|
|
587
|
+
errors.push(`${transitionId}: ${decl.id} references invalid tag ${node.tag}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (node.kind === "freshToken") {
|
|
591
|
+
const decl = varsById.get(node.domainOf);
|
|
592
|
+
if (!decl)
|
|
593
|
+
errors.push(`${transitionId}: freshToken domainOf references unknown var ${node.domainOf}`);
|
|
594
|
+
else if (decl.domain.kind !== "tokens")
|
|
595
|
+
errors.push(`${transitionId}: freshToken domainOf ${node.domainOf} must reference a tokens var`);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function validateReadReference(errors, transitionId, read, varsById) {
|
|
600
|
+
const decl = varsById.get(read.var);
|
|
601
|
+
if (!decl) {
|
|
602
|
+
errors.push(`${transitionId}: expression reads unknown var ${read.var}`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const path = read.path ?? [];
|
|
606
|
+
if (path.length > 0 && !domainAtPath(decl.domain, path)) {
|
|
607
|
+
errors.push(`${transitionId}: ${decl.id} has invalid read path ${path.join(".")}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function validateReadLiteralComparison(errors, transitionId, left, right, varsById) {
|
|
611
|
+
if (left.kind !== "read" || right.kind !== "lit")
|
|
612
|
+
return;
|
|
613
|
+
const decl = varsById.get(left.var);
|
|
614
|
+
if (!decl)
|
|
615
|
+
return;
|
|
616
|
+
const domain = domainAtPath(decl.domain, left.path ?? []);
|
|
617
|
+
if (domain?.kind === "enum" && typeof right.value === "string" && !domain.values.includes(right.value)) {
|
|
618
|
+
errors.push(`${transitionId}: ${decl.id} references invalid enum value ${right.value}`);
|
|
619
|
+
}
|
|
620
|
+
if (domain?.kind === "tagged" && typeof right.value === "object" && right.value !== null && !Array.isArray(right.value)) {
|
|
621
|
+
const tag = right.value[domain.tag];
|
|
622
|
+
if (typeof tag === "string" && !Object.hasOwn(domain.variants, tag)) {
|
|
623
|
+
errors.push(`${transitionId}: ${decl.id} references invalid tag ${tag}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function domainAtPath(domain, path) {
|
|
628
|
+
let current = domain;
|
|
629
|
+
for (const segment of path) {
|
|
630
|
+
if (!current)
|
|
631
|
+
return undefined;
|
|
632
|
+
while (current.kind === "option")
|
|
633
|
+
current = current.inner;
|
|
634
|
+
if (current.kind === "record")
|
|
635
|
+
current = current.fields[segment];
|
|
636
|
+
else if (current.kind === "boundedList") {
|
|
637
|
+
if (!/^\d+$/.test(segment))
|
|
638
|
+
current = undefined;
|
|
639
|
+
else {
|
|
640
|
+
const index = Number(segment);
|
|
641
|
+
current = index >= 0 && index < current.maxLen ? current.inner : undefined;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
else if (current.kind === "tagged")
|
|
645
|
+
current = segment === current.tag ? { kind: "enum", values: Object.keys(current.variants) } : undefined;
|
|
646
|
+
else
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
return current;
|
|
650
|
+
}
|
|
651
|
+
function effectExpressions(effect) {
|
|
652
|
+
switch (effect.kind) {
|
|
653
|
+
case "assign":
|
|
654
|
+
return [effect.expr];
|
|
655
|
+
case "choose":
|
|
656
|
+
return [...effect.among];
|
|
657
|
+
case "if":
|
|
658
|
+
return [effect.cond];
|
|
659
|
+
case "enqueue":
|
|
660
|
+
return Object.values(effect.args);
|
|
661
|
+
case "navigate":
|
|
662
|
+
return effect.to ? [effect.to] : [];
|
|
663
|
+
default:
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function union(sets) {
|
|
668
|
+
const out = new Set();
|
|
669
|
+
for (const set of sets)
|
|
670
|
+
for (const value of set)
|
|
671
|
+
out.add(value);
|
|
672
|
+
return out;
|
|
673
|
+
}
|
|
674
|
+
//# sourceMappingURL=validator.js.map
|