@bramas/simulant-lang 0.0.0-stage → 0.1.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/dist/check.js ADDED
@@ -0,0 +1,540 @@
1
+ /**
2
+ * Typechecking and validation of an experiment's IR: types, inference of
3
+ * nondeterminism (plan 3.1), field ownership (3.5), where parameters may
4
+ * appear, and the memory model (3.7).
5
+ */
6
+ import { BUILTINS } from "./builtins.js";
7
+ import { layout } from "./layout.js";
8
+ import { fieldType, isCollection, isData, isStorable, MOVES, resolvePub, showType, T, typeEq, VERDICTS } from "./types.js";
9
+ class CheckError extends Error {
10
+ node;
11
+ constructor(message, node) {
12
+ super(message);
13
+ this.node = node;
14
+ }
15
+ }
16
+ /** A literal whose type is still to be inferred: `.ctor`, `none`, `[]`. */
17
+ const isHole = (e) => e.e === 'lit' && e.type.k === 'infer';
18
+ export class Checker {
19
+ exp;
20
+ lay;
21
+ errors = [];
22
+ diagnostics = [];
23
+ nondeterministic = new Set();
24
+ resolutions = new Map();
25
+ defState = new Map();
26
+ /** The expressions being inferred, innermost last: errors point at the innermost. */
27
+ stack = [];
28
+ /** Enumerations mentioned anywhere in the experiment, for constructors without an expected type. */
29
+ enums;
30
+ constructor(exp) {
31
+ this.exp = exp;
32
+ this.lay = layout(exp);
33
+ for (const e of this.lay.errors)
34
+ this.report(e);
35
+ this.enums = [T.move, T.verdict, ...enumsIn(exp)];
36
+ }
37
+ report(message, node) {
38
+ this.errors.push(message);
39
+ this.diagnostics.push({ message, node });
40
+ }
41
+ fail(unit, msg) {
42
+ throw new CheckError(`${unit.where}: ${msg}`, this.stack[this.stack.length - 1]);
43
+ }
44
+ /**
45
+ * Checks a root expression; reports its first error. `hint` guides the
46
+ * types of literals without requiring the result to have that type.
47
+ */
48
+ root(expr, scope, unit, expected, hint) {
49
+ try {
50
+ const r = this.infer(expr, new Map(Object.entries(scope)), unit, expected ?? hint);
51
+ if (expected)
52
+ this.expect(r.t, expected, unit, 'result');
53
+ return r;
54
+ }
55
+ catch (e) {
56
+ if (!(e instanceof CheckError))
57
+ throw e;
58
+ this.report(e.message, e.node ?? expr);
59
+ }
60
+ }
61
+ /** The type of a literal of type `infer`, from the expected type or a unique constructor. */
62
+ resolveHole(expr, expected, unit) {
63
+ const v = expr.value;
64
+ const ctorsOf = (t) => t.k === 'enum' ? t.ctors : t.k === 'move' ? [...MOVES] : t.k === 'verdict' ? [...VERDICTS] : undefined;
65
+ let t = expected && expected.k !== 'infer' ? resolvePub(expected, this.lay.pub) : undefined;
66
+ if (t) {
67
+ const ok = typeof v === 'string' ? ctorsOf(t)?.includes(v)
68
+ : v === null ? t.k === 'option' || t.k === 'unit'
69
+ : Array.isArray(v) && v.length === 0 && isCollection(t);
70
+ if (!ok)
71
+ this.fail(unit, `${showHole(v)} is not a value of type ${showType(t)}`);
72
+ }
73
+ else if (typeof v === 'string') {
74
+ const candidates = this.enums.filter(e => ctorsOf(e).includes(v));
75
+ const names = [...new Set(candidates.map(showType))];
76
+ if (!names.length)
77
+ this.fail(unit, `unknown constructor .${v}`);
78
+ if (names.length > 1)
79
+ this.fail(unit, `ambiguous constructor .${v}: it could be ${names.join(' or ')}; write ${names[0]}.${v}`);
80
+ t = candidates[0];
81
+ }
82
+ else {
83
+ this.fail(unit, `cannot infer the type of ${showHole(v)}; add a type ascription, as in (${showHole(v)} : ${v === null ? 'Option Nat' : 'List Nat'})`);
84
+ }
85
+ this.resolutions.set(expr, t);
86
+ return t;
87
+ }
88
+ expect(actual, expected, unit, what) {
89
+ actual = resolvePub(actual, this.lay.pub);
90
+ expected = resolvePub(expected, this.lay.pub);
91
+ if (!typeEq(actual, expected))
92
+ this.fail(unit, `${what}: expected ${showType(expected)}, got ${showType(actual)}`);
93
+ }
94
+ param(name, unit) {
95
+ if (!['init', 'domain'].includes(unit.ctx))
96
+ this.fail(unit, `param ${name} is only allowed in initialisation`);
97
+ const p = this.exp.params.find(p => p.name === name);
98
+ if (!p)
99
+ this.fail(unit, `unknown parameter ${name}`);
100
+ return p.type;
101
+ }
102
+ infer(expr, scope, unit, expected) {
103
+ this.stack.push(expr);
104
+ try {
105
+ return this.inferIn(expr, scope, unit, expected);
106
+ }
107
+ finally {
108
+ this.stack.pop();
109
+ }
110
+ }
111
+ /** Infers the types of `exprs` in order, giving holes the type of the first other one (or `expected`). */
112
+ joined(exprs, scope, unit, expected) {
113
+ const out = new Array(exprs.length);
114
+ let t = expected;
115
+ const order = [...exprs.keys()].sort((a, b) => Number(isHole(exprs[a])) - Number(isHole(exprs[b])));
116
+ for (const i of order) {
117
+ out[i] = this.infer(exprs[i], scope, unit, t);
118
+ t ??= out[i].t;
119
+ }
120
+ return out;
121
+ }
122
+ inferIn(expr, scope, unit, expected) {
123
+ const det = (t) => ({ t, nd: false });
124
+ switch (expr.e) {
125
+ case 'lit':
126
+ if (expr.type.k === 'infer')
127
+ return det(this.resolveHole(expr, expected, unit));
128
+ if (expr.type.k === 'node' && !['init', 'domain'].includes(unit.ctx)) {
129
+ this.fail(unit, 'node literals are only allowed in initialisation');
130
+ }
131
+ return det(expr.type);
132
+ case 'var': {
133
+ const t = scope.get(expr.name);
134
+ if (!t)
135
+ this.fail(unit, `unknown variable ${expr.name}`);
136
+ return det(t);
137
+ }
138
+ case 'param':
139
+ return det(this.param(expr.name, unit));
140
+ case 'let': {
141
+ const v = this.infer(expr.value, scope, unit);
142
+ const b = this.infer(expr.body, new Map(scope).set(expr.name, v.t), unit, expected);
143
+ return { t: b.t, nd: v.nd || b.nd };
144
+ }
145
+ case 'if': {
146
+ const c = this.infer(expr.cond, scope, unit, T.bool);
147
+ this.expect(c.t, T.bool, unit, 'condition');
148
+ const [a, b] = this.joined([expr.then, expr.else], scope, unit, expected);
149
+ this.expect(b.t, a.t, unit, 'else branch');
150
+ return { t: a.t, nd: c.nd || a.nd || b.nd };
151
+ }
152
+ case 'match': {
153
+ const on = this.infer(expr.on, scope, unit);
154
+ const ctors = on.t.k === 'enum' ? on.t.ctors : on.t.k === 'move' ? [...MOVES] : on.t.k === 'verdict' ? [...VERDICTS]
155
+ : this.fail(unit, `cannot match on ${showType(on.t)}`);
156
+ for (const c of Object.keys(expr.cases))
157
+ if (!ctors.includes(c))
158
+ this.fail(unit, `unknown case ${c} of ${showType(on.t)}`);
159
+ if (!expr.else && ctors.some(c => !(c in expr.cases)))
160
+ this.fail(unit, `non-exhaustive match on ${showType(on.t)}`);
161
+ const branches = this.joined([...Object.values(expr.cases), ...(expr.else ? [expr.else] : [])], scope, unit, expected);
162
+ for (const b of branches.slice(1))
163
+ this.expect(b.t, branches[0].t, unit, 'match branch');
164
+ return { t: branches[0].t, nd: on.nd || branches.some(b => b.nd) };
165
+ }
166
+ case 'fn':
167
+ return this.fail(unit, 'a function is only allowed as an argument of a builtin');
168
+ case 'record': {
169
+ let nd = false;
170
+ const target = expected && resolvePub(expected, this.lay.pub);
171
+ const fields = Object.entries(expr.fields).map(([name, e]) => {
172
+ const r = this.infer(e, scope, unit, target ? fieldType(target, name) : undefined);
173
+ nd ||= r.nd;
174
+ return { name, type: r.t };
175
+ });
176
+ return { t: T.record(fields), nd };
177
+ }
178
+ case 'with': {
179
+ const on = this.infer(expr.on, scope, unit);
180
+ let nd = on.nd;
181
+ for (const [name, e] of Object.entries(expr.fields)) {
182
+ const ft = fieldType(on.t, name) ?? this.fail(unit, `${showType(on.t)} has no field ${name}`);
183
+ const r = this.infer(e, scope, unit, ft);
184
+ this.expect(r.t, ft, unit, `field ${name}`);
185
+ nd ||= r.nd;
186
+ }
187
+ return { t: on.t, nd };
188
+ }
189
+ case 'get': {
190
+ // A component reads only its own world fields: `C.world.f`, never `C.world` as a whole.
191
+ if (unit.owner && expr.on.e === 'get' && expr.on.field === 'world') {
192
+ const inner = this.infer(expr.on.on, scope, unit);
193
+ if (inner.t.k === 'config') {
194
+ const f = this.lay.world.find(f => f.name === expr.field) ?? this.fail(unit, `unknown world field ${expr.field}`);
195
+ if (f.owner !== unit.owner)
196
+ this.fail(unit, `world field ${expr.field} belongs to ${f.owner}`);
197
+ return { t: f.type, nd: inner.nd };
198
+ }
199
+ }
200
+ const on = this.infer(expr.on, scope, unit);
201
+ if (unit.owner && on.t.k === 'config' && expr.field === 'world') {
202
+ this.fail(unit, 'a component reads world fields one by one (C.world.f)');
203
+ }
204
+ return { t: this.field(on.t, expr.field, unit), nd: on.nd };
205
+ }
206
+ case 'choose': {
207
+ const where = { task: 'tasks', algorithm: 'algorithms', domain: 'parameter domains' }[unit.ctx];
208
+ if (where)
209
+ this.fail(unit, `choices are not allowed in ${where}`);
210
+ const from = this.infer(expr.from, scope, unit);
211
+ if (!isCollection(from.t))
212
+ this.fail(unit, `choose: expected a List or Multiset, got ${showType(from.t)}`);
213
+ const elem = from.t.of;
214
+ if (expr.such)
215
+ this.lambda(expr.such, [elem], T.bool, scope, unit, 'such');
216
+ if (expr.label)
217
+ this.lambda(expr.label, [elem], T.string, scope, unit, 'label');
218
+ return { t: elem, nd: true };
219
+ }
220
+ case 'effect':
221
+ return this.effect(expr, scope, unit);
222
+ case 'call':
223
+ return this.call(expr, scope, unit, expected);
224
+ }
225
+ }
226
+ field(t, name, unit) {
227
+ switch (t.k) {
228
+ case 'record':
229
+ return fieldType(t, name) ?? this.fail(unit, `${showType(t)} has no field ${name}`);
230
+ case 'config':
231
+ if (name === 'world')
232
+ return this.lay.worldType;
233
+ if (name === 'agents')
234
+ return T.multiset(T.agent);
235
+ return this.fail(unit, `a configuration has no field ${name}`);
236
+ case 'agent': {
237
+ if (name === 'pos')
238
+ return T.node;
239
+ if (name === 'pub')
240
+ return this.lay.pub;
241
+ if (name === 'mem') {
242
+ if (unit.ctx !== 'task')
243
+ this.fail(unit, 'the private memory of an agent can only be read by a task');
244
+ return this.lay.mem;
245
+ }
246
+ const flag = this.lay.flags.find(f => f.name === name);
247
+ const f = flag ?? this.lay.agentFields.find(f => f.name === name) ?? this.fail(unit, `agents have no field ${name}`);
248
+ // Components read their own flags and fields; others' only through `correct` and `present`.
249
+ if (unit.owner && f.owner !== unit.owner)
250
+ this.fail(unit, `${flag ? 'flag' : 'agent field'} ${name} belongs to ${f.owner}`);
251
+ return flag ? T.bool : f.type;
252
+ }
253
+ case 'decision': {
254
+ if (name === 'move')
255
+ return T.move;
256
+ if (name === 'decided')
257
+ return T.bool;
258
+ const a = this.lay.actions.find(a => a.name === name);
259
+ return a?.type ?? this.fail(unit, `decisions have no action ${name}`);
260
+ }
261
+ default:
262
+ return this.fail(unit, `${showType(t)} has no fields`);
263
+ }
264
+ }
265
+ effect(expr, scope, unit) {
266
+ if (!['hook', 'beforeMove', 'def'].includes(unit.ctx))
267
+ this.fail(unit, 'effects are only returned by hooks');
268
+ let nd = false;
269
+ for (const [name, e] of Object.entries(expr.world ?? {})) {
270
+ const f = this.lay.world.find(f => f.name === name) ?? this.fail(unit, `unknown world field ${name}`);
271
+ if (unit.owner && f.owner !== unit.owner)
272
+ this.fail(unit, `world field ${name} belongs to ${f.owner}`);
273
+ const r = this.infer(e, scope, unit, f.type);
274
+ this.expect(r.t, f.type, unit, `world field ${name}`);
275
+ nd ||= r.nd;
276
+ }
277
+ for (const [name, e] of Object.entries(expr.raise ?? {})) {
278
+ const f = this.lay.flags.find(f => f.name === name) ?? this.fail(unit, `unknown flag ${name}`);
279
+ if (unit.owner && f.owner !== unit.owner)
280
+ this.fail(unit, `flag ${name} belongs to ${f.owner}`);
281
+ if (e.e === 'fn') {
282
+ this.lambda(e, [T.agent], T.bool, scope, unit, `raise ${name}`);
283
+ }
284
+ else {
285
+ const r = this.infer(e, scope, unit);
286
+ if (!(isCollection(r.t) && r.t.of.k === 'agent'))
287
+ this.fail(unit, `raise ${name}: expected agents or a predicate on agents`);
288
+ nd ||= r.nd;
289
+ }
290
+ }
291
+ return { t: T.effect, nd };
292
+ }
293
+ /** A deterministic lambda with the given parameter types. Returns its result type. */
294
+ lambda(e, params, ret, scope, unit, what) {
295
+ if (e.e !== 'fn')
296
+ this.fail(unit, `${what}: expected a function`);
297
+ if (e.params.length !== params.length)
298
+ this.fail(unit, `${what}: expected ${params.length} parameter(s)`);
299
+ params = params.map(p => resolvePub(p, this.lay.pub));
300
+ const inner = new Map(scope);
301
+ e.params.forEach((p, i) => {
302
+ if (p.type)
303
+ this.expect(params[i], p.type, unit, `${what}, parameter ${p.name}`);
304
+ inner.set(p.name, params[i]);
305
+ });
306
+ const body = this.infer(e.body, inner, unit);
307
+ if (body.nd)
308
+ this.fail(unit, `${what}: a function passed to a builtin cannot contain choices`);
309
+ if (ret)
310
+ this.expect(body.t, ret, unit, what);
311
+ return body.t;
312
+ }
313
+ call(expr, scope, unit, expected) {
314
+ const sig = BUILTINS[expr.fn];
315
+ if (!sig)
316
+ return this.callDef(expr, scope, unit);
317
+ if (sig.arity !== undefined ? expr.args.length !== sig.arity : expr.args.length === 0) {
318
+ this.fail(unit, `${expr.fn}: wrong number of arguments`);
319
+ }
320
+ if (sig.only && !sig.only.includes(unit.ctx))
321
+ this.fail(unit, `${expr.fn} is not allowed in a ${unit.ctx}`);
322
+ if (sig.config && unit.ctx === 'algorithm')
323
+ this.fail(unit, `${expr.fn}: an algorithm only knows its sensors`);
324
+ const types = [];
325
+ let nd = false;
326
+ // Expected types of arguments, from the other arguments or the expected result.
327
+ const args = expr.args;
328
+ const hinted = (i) => {
329
+ const value = (j) => this.infer(args[j], scope, unit).t;
330
+ switch (expr.fn) {
331
+ case 'eq':
332
+ case 'ne':
333
+ case 'append':
334
+ case 'diff':
335
+ return isHole(args[i]) && !isHole(args[1 - i]) ? value(1 - i) : expr.fn === 'append' ? expected : undefined;
336
+ case 'getD': return i === 1 ? optionOf(types[0]) : undefined;
337
+ case 'mem': return i === 0 && isHole(args[0]) ? elementOf(value(1)) : undefined;
338
+ case 'some': return optionOf(expected);
339
+ case 'list': {
340
+ const other = args.findIndex(a => !isHole(a));
341
+ return elementOf(expected) ?? (isHole(args[i]) && other >= 0 ? value(other) : undefined);
342
+ }
343
+ default: return undefined;
344
+ }
345
+ };
346
+ args.forEach((arg, i) => {
347
+ const lam = sig.lambdas?.[i];
348
+ if (lam) {
349
+ const params = lam(types);
350
+ if (typeof params === 'string')
351
+ this.fail(unit, params);
352
+ const ret = this.lambda(arg, params, undefined, scope, unit, `${expr.fn}, argument ${i + 1}`);
353
+ types.push(T.fn(params, ret));
354
+ }
355
+ else {
356
+ const r = this.infer(arg, scope, unit, hinted(i));
357
+ types.push(r.t);
358
+ nd ||= r.nd;
359
+ }
360
+ });
361
+ const t = sig.ret(types);
362
+ if (typeof t === 'string')
363
+ this.fail(unit, t);
364
+ return { t, nd };
365
+ }
366
+ callDef(expr, scope, unit) {
367
+ const def = unit.defs.get(expr.fn) ?? this.fail(unit, `unknown function ${expr.fn}`);
368
+ if (expr.args.length !== def.params.length)
369
+ this.fail(unit, `${expr.fn}: expected ${def.params.length} argument(s)`);
370
+ let nd = false;
371
+ expr.args.forEach((arg, i) => {
372
+ const r = this.infer(arg, scope, unit, def.params[i].type);
373
+ this.expect(r.t, def.params[i].type, unit, `${expr.fn}, argument ${def.params[i].name}`);
374
+ nd ||= r.nd;
375
+ });
376
+ const body = this.checkDef(def, unit);
377
+ if (body.nd && ['task', 'algorithm', 'domain'].includes(unit.ctx))
378
+ this.fail(unit, `${expr.fn} contains choices`);
379
+ return { t: resolvePub(def.ret, this.lay.pub), nd: nd || body.nd };
380
+ }
381
+ checkDef(def, caller) {
382
+ const state = this.defState.get(def);
383
+ if (state === 'checking')
384
+ this.fail(caller, `${def.name} is recursive`);
385
+ if (state)
386
+ return state;
387
+ this.defState.set(def, 'checking');
388
+ const unit = { ctx: 'def', where: `def ${def.name}`, owner: caller.owner, defs: caller.defs };
389
+ const r = this.infer(def.body, new Map(def.params.map(p => [p.name, resolvePub(p.type, this.lay.pub)])), unit);
390
+ this.expect(r.t, def.ret, unit, 'result');
391
+ this.defState.set(def, r);
392
+ if (r.nd)
393
+ this.nondeterministic.add(def.name);
394
+ return r;
395
+ }
396
+ check() {
397
+ const { exp, lay } = this;
398
+ const { model, algorithm, task } = exp;
399
+ const defsOf = (...lists) => new Map(lists.flatMap(l => l ?? []).map(d => [d.name, d]));
400
+ const modelDefs = defsOf(model.defs);
401
+ const unit = (ctx, where, defs = modelDefs, owner) => ({ ctx, where, defs, owner });
402
+ const sensorScope = { C: T.config, me: T.agent, frame: T.frame };
403
+ for (const p of exp.params) {
404
+ if (!isStorable(p.type))
405
+ this.report(`parameter ${p.name}: ${showType(p.type)} cannot be stored`);
406
+ if (p.domain)
407
+ this.root(p.domain, {}, unit('domain', `domain of ${p.name}`), T.list(p.type));
408
+ }
409
+ this.root(model.topology.size, {}, unit('domain', 'topology size'), T.nat);
410
+ this.root(exp.agents.count, {}, unit('init', 'agent count'), T.nat);
411
+ this.root(exp.agents.pos, { i: T.nat }, unit('init', 'agent positions'), T.node);
412
+ for (const s of model.sensors)
413
+ this.root(s.body, sensorScope, unit('sensor', `sensor ${s.name}`), s.type);
414
+ for (const c of model.components) {
415
+ const defs = defsOf(model.defs, c.defs);
416
+ const params = Object.fromEntries((c.params ?? []).map(p => [p.name, p.type]));
417
+ for (const p of c.params ?? []) {
418
+ if (!isStorable(p.type))
419
+ this.report(`${c.name}: parameter ${p.name} cannot be stored`);
420
+ if (!(p.name in (c.args ?? {})))
421
+ this.report(`${c.name}: no value for parameter ${p.name}`);
422
+ }
423
+ const sensorScope = { ...params, C: T.config, me: T.agent, frame: T.frame };
424
+ for (const s of c.sensors ?? [])
425
+ this.root(s.body, sensorScope, unit('sensor', `${c.name}.${s.name}`, defs, c.name), s.type);
426
+ for (const f of c.world ?? []) {
427
+ if (!isStorable(f.type))
428
+ this.report(`${c.name}: world field ${f.name} cannot be stored`);
429
+ const init = c.init?.world?.[f.name];
430
+ if (!init)
431
+ this.report(`${c.name}: world field ${f.name} has no initial value`);
432
+ else
433
+ this.root(init, params, unit('init', `${c.name}.init.${f.name}`, defs, c.name), f.type);
434
+ }
435
+ for (const f of c.agent ?? []) {
436
+ if (!isStorable(f.type))
437
+ this.report(`${c.name}: agent field ${f.name} cannot be stored`);
438
+ const init = c.init?.agent?.[f.name];
439
+ if (!init)
440
+ this.report(`${c.name}: agent field ${f.name} has no initial value`);
441
+ else
442
+ this.root(init, { ...params, i: T.nat }, unit('init', `${c.name}.init.${f.name}`, defs, c.name), f.type);
443
+ }
444
+ for (const name of Object.keys(c.init?.world ?? {})) {
445
+ if (!c.world?.some(f => f.name === name))
446
+ this.report(`${c.name}: initial value for unknown world field ${name}`);
447
+ }
448
+ for (const name of Object.keys(c.init?.agent ?? {})) {
449
+ if (!c.agent?.some(f => f.name === name))
450
+ this.report(`${c.name}: initial value for unknown agent field ${name}`);
451
+ }
452
+ for (const hook of ['beforeLook', 'beforeMove', 'afterMove']) {
453
+ const body = c[hook];
454
+ if (body)
455
+ this.root(body, { ...params, C: T.config }, unit(hook === 'beforeMove' ? 'beforeMove' : 'hook', `${c.name}.${hook}`, defs, c.name), T.effect);
456
+ }
457
+ }
458
+ if (model.frame.kind === 'persistent') {
459
+ const name = model.frame.field;
460
+ const f = lay.agentFields.find(f => f.name === name);
461
+ if (!f || f.type.k !== 'frame')
462
+ this.report(`frame model: agent field ${name} of type Frame does not exist`);
463
+ }
464
+ // Algorithm: only its sensors; deterministic; memory model.
465
+ const algDefs = defsOf(model.defs, algorithm.defs);
466
+ if (!isStorable(algorithm.pub))
467
+ this.report(`algorithm ${algorithm.name}: public state cannot be stored`);
468
+ if (!isStorable(algorithm.mem))
469
+ this.report(`algorithm ${algorithm.name}: memory cannot be stored`);
470
+ if (model.memory === 'oblivious' && algorithm.mem.k !== 'unit') {
471
+ this.report(`algorithm ${algorithm.name}: the model is oblivious, but the algorithm keeps memory of type ${showType(algorithm.mem)}`);
472
+ }
473
+ this.root(algorithm.init.pub, { i: T.nat }, unit('algorithm', `${algorithm.name}.init.pub`, algDefs), algorithm.pub);
474
+ this.root(algorithm.init.mem, { i: T.nat }, unit('algorithm', `${algorithm.name}.init.mem`, algDefs), algorithm.mem);
475
+ if (!algorithm.native) {
476
+ // A step may omit actions (they keep their defaults): the decision type is only a hint.
477
+ const step = this.root(algorithm.step, { mem: algorithm.mem, pub: algorithm.pub, view: lay.view }, unit('algorithm', `${algorithm.name}.step`, algDefs), undefined, lay.decision);
478
+ if (step)
479
+ this.checkDecision(step.t, `${algorithm.name}.step`);
480
+ }
481
+ if (algorithm.line)
482
+ this.root(algorithm.line, { mem: algorithm.mem }, unit('algorithm', `${algorithm.name}.line`, algDefs), T.nat);
483
+ this.root(task.verdict, { C: T.config }, unit('task', `task ${task.name}`, defsOf(model.defs, task.defs)), T.verdict);
484
+ return { errors: this.errors, diagnostics: this.diagnostics, layout: lay, nondeterministic: this.nondeterministic, resolutions: this.resolutions };
485
+ }
486
+ /** A step returns `move`, `pub` and `mem`, and possibly some actions. */
487
+ checkDecision(t, where) {
488
+ if (t.k !== 'record') {
489
+ this.report(`${where}: expected a decision record, got ${showType(t)}`);
490
+ return;
491
+ }
492
+ const expected = this.lay.decision;
493
+ for (const f of t.fields) {
494
+ const e = expected.fields.find(g => g.name === f.name);
495
+ if (!e)
496
+ this.report(`${where}: unknown decision field ${f.name}`);
497
+ else if (!typeEq(e.type, f.type))
498
+ this.report(`${where}: decision field ${f.name}: expected ${showType(e.type)}, got ${showType(f.type)}`);
499
+ }
500
+ for (const name of ['move', 'pub', 'mem']) {
501
+ if (!t.fields.some(f => f.name === name))
502
+ this.report(`${where}: the decision has no ${name}`);
503
+ }
504
+ }
505
+ }
506
+ export function check(exp) {
507
+ if (exp.irVersion !== 1) {
508
+ const message = `unsupported IR version ${exp.irVersion}`;
509
+ return { errors: [message], diagnostics: [{ message }], layout: layout(exp), nondeterministic: new Set(), resolutions: new Map() };
510
+ }
511
+ return new Checker(exp).check();
512
+ }
513
+ function showHole(v) {
514
+ return typeof v === 'string' ? `.${v}` : v === null ? 'none' : '[]';
515
+ }
516
+ function optionOf(t) {
517
+ return t?.k === 'option' ? t.of : undefined;
518
+ }
519
+ function elementOf(t) {
520
+ return t && isCollection(t) ? t.of : undefined;
521
+ }
522
+ /** Every enumeration type mentioned in the experiment. */
523
+ function enumsIn(exp) {
524
+ const found = new Map();
525
+ const visit = (x) => {
526
+ if (!x || typeof x !== 'object')
527
+ return;
528
+ if (Array.isArray(x)) {
529
+ x.forEach(visit);
530
+ return;
531
+ }
532
+ const o = x;
533
+ if (o.k === 'enum')
534
+ found.set(JSON.stringify(o), o);
535
+ for (const v of Object.values(o))
536
+ visit(v);
537
+ };
538
+ visit(exp);
539
+ return [...found.values()];
540
+ }