@bramas/simulant-engine 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.
@@ -0,0 +1,640 @@
1
+ /**
2
+ * Compiles IR expressions to JavaScript closures.
3
+ *
4
+ * A deterministic expression becomes `(frame) => value`. An expression that
5
+ * contains a choice becomes `(frame, k) => void`, which calls `k` once per
6
+ * outcome, in the order of the choices (continuation-passing style). While
7
+ * `k` runs, `rt.witness` holds the choices that led to the outcome.
8
+ *
9
+ * Variables live in slots of a frame array, allocated statically. Because
10
+ * evaluation is synchronous and continuations finish before the next outcome
11
+ * is produced, a slot can simply be overwritten for each outcome.
12
+ */
13
+ import { BUILTINS } from "@bramas/simulant-lang/builtins";
14
+ import { fieldType, isCollection, resolvePub, showType, T } from "@bramas/simulant-lang/types";
15
+ import { opposite } from "./topology.js";
16
+ import { canonicalSort, encoder, equality, show } from "./values.js";
17
+ /** At most this many calls are recorded (definitions called in lambdas over a whole ring add up). */
18
+ const MAX_TRACED_CALLS = 2000;
19
+ export class Runtime {
20
+ lay;
21
+ params = {};
22
+ topo;
23
+ topoState = null;
24
+ decisions = [];
25
+ witness = [];
26
+ /** Set only while an explanation evaluates (by a compiler built with `traced`). */
27
+ trace;
28
+ flagNames;
29
+ hiddenFlags;
30
+ neverFlags;
31
+ adversaryFlags;
32
+ constructor(lay) {
33
+ this.lay = lay;
34
+ this.flagNames = lay.flags.map(f => f.name);
35
+ this.hiddenFlags = lay.flags.filter(f => f.visibility === 'hidden').map(f => f.name);
36
+ this.neverFlags = lay.flags.filter(f => f.acts === 'never').map(f => f.name);
37
+ this.adversaryFlags = lay.flags.filter(f => f.acts === 'adversary').map(f => f.name);
38
+ }
39
+ correct(a) {
40
+ for (const f of this.flagNames)
41
+ if (a.f[f])
42
+ return false;
43
+ return true;
44
+ }
45
+ present(a) {
46
+ for (const f of this.hiddenFlags)
47
+ if (a.f[f])
48
+ return false;
49
+ return true;
50
+ }
51
+ /** Who decides for the agent: never > adversary > algorithm. */
52
+ acts(a) {
53
+ for (const f of this.neverFlags)
54
+ if (a.f[f])
55
+ return 'never';
56
+ for (const f of this.adversaryFlags)
57
+ if (a.f[f])
58
+ return 'adversary';
59
+ return 'algorithm';
60
+ }
61
+ }
62
+ const toND = (c) => c.nd ?? ((fr, k) => k(c.det(fr)));
63
+ /** Evaluates expressions in order, possibly with choices, then calls `k` with their values. */
64
+ function sequence(cs) {
65
+ const nds = cs.map(toND);
66
+ const vals = new Array(cs.length);
67
+ const step = (i, fr, k) => {
68
+ if (i === nds.length)
69
+ return k(vals.slice());
70
+ nds[i](fr, v => { vals[i] = v; step(i + 1, fr, k); });
71
+ };
72
+ return (fr, k) => step(0, fr, k);
73
+ }
74
+ export class CompileError extends Error {
75
+ }
76
+ export class Compiler {
77
+ rt;
78
+ lay;
79
+ paramTypes;
80
+ traced;
81
+ defFns = new Map();
82
+ constructor(rt, lay, paramTypes,
83
+ /** Records, in `rt.trace`, the lines of values and the calls of definitions (explanations only). */
84
+ traced = false) {
85
+ this.rt = rt;
86
+ this.lay = lay;
87
+ this.paramTypes = paramTypes;
88
+ this.traced = traced;
89
+ }
90
+ /** Compiles `expr` with variables in slots 0, 1, ... in the order given. */
91
+ root(expr, vars, defs) {
92
+ const rc = { n: vars.length };
93
+ const scope = { vars: new Map() };
94
+ vars.forEach(([name, t], slot) => {
95
+ scope.vars.set(name, { slot, t: this.resolve(t) });
96
+ if (t.k === 'config')
97
+ scope.config = slot;
98
+ });
99
+ const c = this.compile(expr, scope, rc, defs);
100
+ return { t: c.t, nslots: rc.n, det: c.det, nd: c.nd };
101
+ }
102
+ resolve(t) {
103
+ return resolvePub(t, this.lay.pub);
104
+ }
105
+ fail(msg) {
106
+ throw new CompileError(msg);
107
+ }
108
+ compile(expr, scope, rc, defs) {
109
+ const out = this.compileNode(expr, scope, rc, defs);
110
+ const at = expr.at;
111
+ // A traced value remembers where it came from; conditionals and lets pass on their branch's.
112
+ if (!this.traced || at === undefined || expr.e === 'if' || expr.e === 'match' || expr.e === 'let')
113
+ return out;
114
+ const rt = this.rt;
115
+ const { det, nd } = out;
116
+ return {
117
+ t: out.t,
118
+ det: det && (fr => { const v = det(fr); if (rt.trace)
119
+ rt.trace.line = at; return v; }),
120
+ nd: nd && ((fr, k) => nd(fr, v => { if (rt.trace)
121
+ rt.trace.line = at; k(v); })),
122
+ };
123
+ }
124
+ compileNode(expr, scope, rc, defs) {
125
+ const rt = this.rt;
126
+ const c = (e, s = scope) => this.compile(e, s, rc, defs);
127
+ switch (expr.e) {
128
+ case 'lit': {
129
+ const t = this.resolve(expr.type);
130
+ const value = t.k === 'multiset' ? canonicalSort(t.of)(expr.value) : expr.value;
131
+ return { t, det: () => value };
132
+ }
133
+ case 'var': {
134
+ const v = scope.vars.get(expr.name) ?? this.fail(`unknown variable ${expr.name}`);
135
+ const slot = v.slot;
136
+ return { t: v.t, det: fr => fr[slot] };
137
+ }
138
+ case 'param': {
139
+ const t = this.paramTypes.get(expr.name) ?? this.fail(`unknown parameter ${expr.name}`);
140
+ const name = expr.name;
141
+ return { t, det: () => rt.params[name] };
142
+ }
143
+ case 'let': {
144
+ const v = c(expr.value);
145
+ const slot = rc.n++;
146
+ const inner = { vars: new Map(scope.vars).set(expr.name, { slot, t: v.t }), config: v.t.k === 'config' ? slot : scope.config };
147
+ const b = c(expr.body, inner);
148
+ if (v.det && b.det) {
149
+ const vd = v.det, bd = b.det;
150
+ return { t: b.t, det: fr => { fr[slot] = vd(fr); return bd(fr); } };
151
+ }
152
+ const vn = toND(v), bn = toND(b);
153
+ return { t: b.t, nd: (fr, k) => vn(fr, x => { fr[slot] = x; bn(fr, k); }) };
154
+ }
155
+ case 'if': {
156
+ const [cond, a, b] = [c(expr.cond), c(expr.then), c(expr.else)];
157
+ if (cond.det && a.det && b.det) {
158
+ const cd = cond.det, ad = a.det, bd = b.det;
159
+ return { t: a.t, det: fr => cd(fr) ? ad(fr) : bd(fr) };
160
+ }
161
+ const cn = toND(cond), an = toND(a), bn = toND(b);
162
+ return { t: a.t, nd: (fr, k) => cn(fr, v => (v ? an : bn)(fr, k)) };
163
+ }
164
+ case 'match': {
165
+ const on = c(expr.on);
166
+ const cases = Object.fromEntries(Object.entries(expr.cases).map(([ctor, e]) => [ctor, c(e)]));
167
+ const otherwise = expr.else ? c(expr.else) : undefined;
168
+ const branches = [...Object.values(cases), ...(otherwise ? [otherwise] : [])];
169
+ const t = branches[0].t;
170
+ if (on.det && branches.every(b => b.det)) {
171
+ const od = on.det;
172
+ const table = new Map(Object.entries(cases).map(([ctor, b]) => [ctor, b.det]));
173
+ const dflt = otherwise?.det;
174
+ return { t, det: fr => (table.get(od(fr)) ?? dflt)(fr) };
175
+ }
176
+ const onN = toND(on);
177
+ const table = new Map(Object.entries(cases).map(([ctor, b]) => [ctor, toND(b)]));
178
+ const dflt = otherwise && toND(otherwise);
179
+ return { t, nd: (fr, k) => onN(fr, v => (table.get(v) ?? dflt)(fr, k)) };
180
+ }
181
+ case 'fn':
182
+ return this.fail('a function is only allowed as an argument of a builtin');
183
+ case 'record': {
184
+ const names = Object.keys(expr.fields);
185
+ const cs = names.map(n => c(expr.fields[n]));
186
+ const t = T.record(names.map((name, i) => ({ name, type: cs[i].t })));
187
+ const build = (vs) => {
188
+ const o = {};
189
+ for (let i = 0; i < names.length; i++)
190
+ o[names[i]] = vs[i];
191
+ return o;
192
+ };
193
+ return this.lift(t, cs, build);
194
+ }
195
+ case 'with': {
196
+ const on = c(expr.on);
197
+ const names = Object.keys(expr.fields);
198
+ const cs = [on, ...names.map(n => c(expr.fields[n]))];
199
+ return this.lift(on.t, cs, ([base, ...vs]) => {
200
+ const o = { ...base };
201
+ for (let i = 0; i < names.length; i++)
202
+ o[names[i]] = vs[i];
203
+ return o;
204
+ });
205
+ }
206
+ case 'get':
207
+ return this.get(c(expr.on), expr.field, scope);
208
+ case 'choose':
209
+ return this.choose(expr, scope, rc, defs);
210
+ case 'effect':
211
+ return this.effect(expr, scope, rc, defs);
212
+ case 'call':
213
+ return BUILTINS[expr.fn] ? this.builtin(expr, scope, rc, defs) : this.callDef(expr, scope, rc, defs);
214
+ }
215
+ }
216
+ /** Applies a deterministic function to the values of `cs`, which may contain choices. */
217
+ lift(t, cs, f) {
218
+ if (cs.every(x => x.det)) {
219
+ const ds = cs.map(x => x.det);
220
+ if (ds.length === 1) {
221
+ const d = ds[0];
222
+ return { t, det: fr => f([d(fr)]) };
223
+ }
224
+ return { t, det: fr => f(ds.map(d => d(fr))) };
225
+ }
226
+ const seq = sequence(cs);
227
+ return { t, nd: (fr, k) => seq(fr, vs => k(f(vs))) };
228
+ }
229
+ configSlot(scope, what) {
230
+ if (scope.config === undefined)
231
+ this.fail(`${what} needs a configuration in scope`);
232
+ return scope.config;
233
+ }
234
+ get(on, field, scope) {
235
+ const t = on.t;
236
+ switch (t.k) {
237
+ case 'record': {
238
+ const ft = fieldType(t, field) ?? this.fail(`${showType(t)} has no field ${field}`);
239
+ return this.lift(ft, [on], ([v]) => v[field]);
240
+ }
241
+ case 'config':
242
+ if (field === 'world')
243
+ return this.lift(this.lay.worldType, [on], ([C]) => C.world);
244
+ if (field === 'agents')
245
+ return this.lift(T.multiset(T.agent), [on], ([C]) => agentIndices(C));
246
+ return this.fail(`a configuration has no field ${field}`);
247
+ case 'agent': {
248
+ const cs = this.configSlot(scope, `reading ${field} of an agent`);
249
+ let ft;
250
+ let read;
251
+ if (field === 'pos') {
252
+ ft = T.node;
253
+ read = a => a.pos;
254
+ }
255
+ else if (field === 'pub') {
256
+ ft = this.lay.pub;
257
+ read = a => a.pub;
258
+ }
259
+ else if (field === 'mem') {
260
+ ft = this.lay.mem;
261
+ read = a => a.mem;
262
+ }
263
+ else if (this.lay.flags.some(f => f.name === field)) {
264
+ ft = T.bool;
265
+ read = a => !!a.f[field];
266
+ }
267
+ else {
268
+ ft = this.lay.agentFields.find(f => f.name === field)?.type ?? this.fail(`agents have no field ${field}`);
269
+ read = a => a.f[field];
270
+ }
271
+ if (on.det) {
272
+ const d = on.det;
273
+ return { t: ft, det: fr => read(fr[cs].agents[d(fr)]) };
274
+ }
275
+ const n = toND(on);
276
+ return { t: ft, nd: (fr, k) => n(fr, i => k(read(fr[cs].agents[i]))) };
277
+ }
278
+ case 'decision': {
279
+ const ft = field === 'move' ? T.move : field === 'decided' ? T.bool
280
+ : this.lay.actions.find(a => a.name === field)?.type ?? this.fail(`decisions have no action ${field}`);
281
+ return this.lift(ft, [on], ([d]) => d[field]);
282
+ }
283
+ default:
284
+ return this.fail(`${showType(t)} has no fields`);
285
+ }
286
+ }
287
+ /** A deterministic lambda: `(fr, ...args) => value`. */
288
+ lambda(e, params, scope, rc, defs) {
289
+ if (e.e !== 'fn')
290
+ this.fail('expected a function');
291
+ const vars = new Map(scope.vars);
292
+ const slots = e.params.map((p, i) => {
293
+ const slot = rc.n++;
294
+ vars.set(p.name, { slot, t: this.resolve(params[i]) });
295
+ return slot;
296
+ });
297
+ const body = this.compile(e.body, { vars, config: scope.config }, rc, defs);
298
+ if (!body.det)
299
+ this.fail('a function passed to a builtin cannot contain choices');
300
+ const d = body.det;
301
+ if (slots.length === 1) {
302
+ const s = slots[0];
303
+ return { t: body.t, fn: (fr, x) => { fr[s] = x; return d(fr); } };
304
+ }
305
+ return { t: body.t, fn: (fr, ...xs) => { slots.forEach((s, i) => { fr[s] = xs[i]; }); return d(fr); } };
306
+ }
307
+ choose(expr, scope, rc, defs) {
308
+ const rt = this.rt;
309
+ const from = this.compile(expr.from, scope, rc, defs);
310
+ if (!isCollection(from.t))
311
+ this.fail('choose: expected a collection');
312
+ const elem = from.t.of;
313
+ const such = expr.such && this.lambda(expr.such, [elem], scope, rc, defs).fn;
314
+ const label = expr.label ? this.lambda(expr.label, [elem], scope, rc, defs).fn : undefined;
315
+ const showElem = show(elem);
316
+ const fromN = toND(from);
317
+ const site = expr.site;
318
+ return {
319
+ t: elem,
320
+ nd: (fr, k) => fromN(fr, (xs) => {
321
+ for (let i = 0; i < xs.length; i++) {
322
+ const x = xs[i];
323
+ if (such && !such(fr, x))
324
+ continue;
325
+ rt.witness.push({ site, index: i, label: label ? label(fr, x) : showElem(x) });
326
+ k(x);
327
+ rt.witness.pop();
328
+ }
329
+ }),
330
+ };
331
+ }
332
+ effect(expr, scope, rc, defs) {
333
+ const worldNames = Object.keys(expr.world ?? {});
334
+ const world = worldNames.map(n => this.compile(expr.world[n], scope, rc, defs));
335
+ const raiseNames = Object.keys(expr.raise ?? {});
336
+ const cs = raiseNames.length ? this.configSlot(scope, 'raising flags') : -1;
337
+ // Each raise becomes a function from the frame to "which agents raise the flag".
338
+ const raises = raiseNames.map(n => {
339
+ const e = expr.raise[n];
340
+ if (e.e === 'fn') {
341
+ const pred = this.lambda(e, [T.agent], scope, rc, defs).fn;
342
+ return { t: T.unit, det: fr => fr[cs].agents.map((_, i) => !!pred(fr, i)) };
343
+ }
344
+ return this.lift(T.unit, [this.compile(e, scope, rc, defs)], ([idx]) => indexMarks(idx));
345
+ });
346
+ const all = [...world, ...raises];
347
+ return this.lift(T.effect, all, vs => {
348
+ const eff = { world: {}, raise: {} };
349
+ worldNames.forEach((n, i) => { eff.world[n] = vs[i]; });
350
+ raiseNames.forEach((n, j) => { eff.raise[n] = vs[worldNames.length + j]; });
351
+ return eff;
352
+ });
353
+ }
354
+ callDef(expr, scope, rc, defs) {
355
+ const def = defs.get(expr.fn) ?? this.fail(`unknown function ${expr.fn}`);
356
+ let fn = this.defFns.get(def);
357
+ if (!fn) {
358
+ fn = this.root(def.body, def.params.map(p => [p.name, p.type]), defs);
359
+ this.defFns.set(def, fn);
360
+ }
361
+ const { nslots, det, nd } = fn;
362
+ const args = expr.args.map(a => this.compile(a, scope, rc, defs));
363
+ if (det && args.every(a => a.det)) {
364
+ const ds = args.map(a => a.det);
365
+ const run = this.traced ? this.tracedCall(def.name, args.map(a => show(a.t, true)), show(this.resolve(def.ret), true), det) : det;
366
+ return {
367
+ t: this.resolve(def.ret),
368
+ det: fr => {
369
+ const f2 = new Array(nslots);
370
+ for (let i = 0; i < ds.length; i++)
371
+ f2[i] = ds[i](fr);
372
+ return run(f2);
373
+ },
374
+ };
375
+ }
376
+ const seq = sequence(args);
377
+ const body = nd ?? ((f2, k) => k(det(f2)));
378
+ return {
379
+ t: this.resolve(def.ret),
380
+ nd: (fr, k) => seq(fr, vs => {
381
+ const f2 = new Array(nslots);
382
+ vs.forEach((v, i) => { f2[i] = v; });
383
+ body(f2, k);
384
+ }),
385
+ };
386
+ }
387
+ /** A definition's body that records its call in `rt.trace` (arguments, result, line it returned from). */
388
+ tracedCall(fn, showArgs, showRet, body) {
389
+ const rt = this.rt;
390
+ return f2 => {
391
+ const trace = rt.trace;
392
+ if (!trace)
393
+ return body(f2);
394
+ const call = { fn, args: showArgs.map((s, i) => s(f2[i])), result: '', calls: [] };
395
+ trace.stack.push(call.calls);
396
+ let v;
397
+ try {
398
+ v = body(f2);
399
+ }
400
+ finally {
401
+ trace.stack.pop();
402
+ }
403
+ call.result = showRet(v);
404
+ call.line = trace.line;
405
+ if (trace.count++ < MAX_TRACED_CALLS)
406
+ trace.stack[trace.stack.length - 1].push(call);
407
+ return v;
408
+ };
409
+ }
410
+ builtin(expr, scope, rc, defs) {
411
+ if (expr.fn === 'correct' || expr.fn === 'present') {
412
+ // Like a field of the agent: read in the configuration in scope.
413
+ const cs = this.configSlot(scope, expr.fn);
414
+ const test = expr.fn === 'correct' ? (a) => this.rt.correct(a) : (a) => this.rt.present(a);
415
+ const a = this.compile(expr.args[0], scope, rc, defs);
416
+ if (a.det) {
417
+ const d = a.det;
418
+ return { t: T.bool, det: fr => test(fr[cs].agents[d(fr)]) };
419
+ }
420
+ const n = toND(a);
421
+ return { t: T.bool, nd: (fr, k) => n(fr, i => k(test(fr[cs].agents[i]))) };
422
+ }
423
+ const sig = BUILTINS[expr.fn];
424
+ const types = [];
425
+ const parts = [];
426
+ expr.args.forEach((arg, i) => {
427
+ const lam = sig.lambdas?.[i];
428
+ if (lam) {
429
+ const params = lam(types);
430
+ if (typeof params === 'string')
431
+ this.fail(params);
432
+ const l = this.lambda(arg, params, scope, rc, defs);
433
+ types.push(T.fn(params, l.t));
434
+ parts.push({ kind: 'fn', fn: l.fn });
435
+ }
436
+ else {
437
+ const c = this.compile(arg, scope, rc, defs);
438
+ types.push(c.t);
439
+ parts.push({ kind: 'value', c });
440
+ }
441
+ });
442
+ const t = sig.ret(types);
443
+ if (typeof t === 'string')
444
+ this.fail(t);
445
+ const impl = this.impl(expr.fn, types, t, scope);
446
+ const values = parts.filter(p => p.kind === 'value').map(p => p.c);
447
+ const fns = parts.map(p => p.kind === 'fn' ? p.fn : undefined);
448
+ const hasFns = fns.some(f => f);
449
+ // Lambdas receive the current frame; values are evaluated first, in order.
450
+ const call = (fr, vs) => {
451
+ if (!hasFns)
452
+ return impl(vs);
453
+ let j = 0;
454
+ const args = fns.map(f => f ? (...xs) => f(fr, ...xs) : vs[j++]);
455
+ return impl(args);
456
+ };
457
+ if (values.every(v => v.det)) {
458
+ const ds = values.map(v => v.det);
459
+ if (expr.fn === 'and')
460
+ return { t, det: fr => { for (const d of ds)
461
+ if (!d(fr))
462
+ return false; return true; } };
463
+ if (expr.fn === 'or')
464
+ return { t, det: fr => { for (const d of ds)
465
+ if (d(fr))
466
+ return true; return false; } };
467
+ if (ds.length === 1) {
468
+ const d = ds[0];
469
+ return { t, det: fr => call(fr, [d(fr)]) };
470
+ }
471
+ if (ds.length === 2) {
472
+ const [d0, d1] = ds;
473
+ return { t, det: fr => call(fr, [d0(fr), d1(fr)]) };
474
+ }
475
+ return { t, det: fr => call(fr, ds.map(d => d(fr))) };
476
+ }
477
+ const seq = sequence(values);
478
+ return { t, nd: (fr, k) => seq(fr, vs => k(call(fr, vs))) };
479
+ }
480
+ /** The implementation of a builtin, specialised to its argument types. */
481
+ impl(fn, types, ret, scope) {
482
+ const rt = this.rt;
483
+ const elemOf = (t) => t.of;
484
+ const isMultiset = types[0]?.k === 'multiset';
485
+ const nat = types[0]?.k === 'nat';
486
+ switch (fn) {
487
+ case 'not': return ([x]) => !x;
488
+ case 'and': return xs => xs.every(Boolean);
489
+ case 'or': return xs => xs.some(Boolean);
490
+ case 'implies': return ([a, b]) => !a || b;
491
+ case 'eq': {
492
+ const eq = equality(types[0]);
493
+ return ([a, b]) => eq(a, b);
494
+ }
495
+ case 'ne': {
496
+ const eq = equality(types[0]);
497
+ return ([a, b]) => !eq(a, b);
498
+ }
499
+ case 'lt': return types[0].k === 'list' ? ([a, b]) => lexCompare(a, b) < 0 : ([a, b]) => a < b;
500
+ case 'le': return types[0].k === 'list' ? ([a, b]) => lexCompare(a, b) <= 0 : ([a, b]) => a <= b;
501
+ case 'gt': return types[0].k === 'list' ? ([a, b]) => lexCompare(a, b) > 0 : ([a, b]) => a > b;
502
+ case 'ge': return types[0].k === 'list' ? ([a, b]) => lexCompare(a, b) >= 0 : ([a, b]) => a >= b;
503
+ case 'add': return ([a, b]) => a + b;
504
+ case 'sub': return nat ? ([a, b]) => Math.max(0, a - b) : ([a, b]) => a - b;
505
+ case 'mul': return ([a, b]) => a * b;
506
+ // Lean: division by zero is zero; on Int, `/` and `%` are Euclidean.
507
+ case 'div': return nat ? ([a, b]) => b === 0 ? 0 : Math.floor(a / b) : ([a, b]) => b === 0 ? 0 : Math.sign(b) * Math.floor(a / Math.abs(b));
508
+ case 'mod': return nat ? ([a, b]) => b === 0 ? a : a % b : ([a, b]) => b === 0 ? a : ((a % Math.abs(b)) + Math.abs(b)) % Math.abs(b);
509
+ case 'min': return ([a, b]) => Math.min(a, b);
510
+ case 'max': return ([a, b]) => Math.max(a, b);
511
+ case 'toInt': return ([a]) => a;
512
+ case 'some': return ([x]) => x;
513
+ case 'isSome': return ([o]) => o !== null;
514
+ case 'isNone': return ([o]) => o === null;
515
+ case 'getD': return ([o, d]) => o === null ? d : o;
516
+ case 'length': return ([l]) => l.length;
517
+ case 'isEmpty': return ([l]) => l.length === 0;
518
+ case 'filter': return ([l, p]) => l.filter((x) => p(x));
519
+ case 'any': return ([l, p]) => l.some((x) => p(x));
520
+ case 'all': return ([l, p]) => l.every((x) => p(x));
521
+ case 'count': return ([l, p]) => { let n = 0; for (const x of l)
522
+ if (p(x))
523
+ n++; return n; };
524
+ case 'map': {
525
+ if (!isMultiset)
526
+ return ([l, f]) => l.map((x) => f(x));
527
+ const sort = canonicalSort(elemOf(ret));
528
+ return ([l, f]) => sort(l.map((x) => f(x)));
529
+ }
530
+ case 'sum': return ([l]) => l.reduce((a, b) => a + b, 0);
531
+ case 'mem': {
532
+ const eq = equality(types[0]);
533
+ return ([x, l]) => l.some((y) => eq(x, y));
534
+ }
535
+ case 'range': return ([n]) => Array.from({ length: n }, (_, i) => i);
536
+ case 'toMultiset': {
537
+ const sort = canonicalSort(elemOf(types[0]));
538
+ return ([l]) => sort(l);
539
+ }
540
+ case 'toList': return ([l]) => l.slice();
541
+ case 'subsets': return ([l]) => subsetsOf(l, l.length, types[0]);
542
+ case 'subsetsUpTo': return ([l, k]) => subsetsOf(l, k, types[0]);
543
+ case 'dedup': {
544
+ const enc = encoder(elemOf(types[0]));
545
+ return ([l]) => { const seen = new Set(); return l.filter((x) => { const e = enc(x); if (seen.has(e))
546
+ return false; seen.add(e); return true; }); };
547
+ }
548
+ case 'diff': {
549
+ const enc = encoder(elemOf(types[0]));
550
+ return ([a, b]) => {
551
+ const remove = new Map();
552
+ for (const x of b) {
553
+ const e = enc(x);
554
+ remove.set(e, (remove.get(e) ?? 0) + 1);
555
+ }
556
+ return a.filter((x) => {
557
+ const e = enc(x);
558
+ const n = remove.get(e);
559
+ if (!n)
560
+ return true;
561
+ remove.set(e, n - 1);
562
+ return false;
563
+ });
564
+ };
565
+ }
566
+ case 'list': return xs => xs;
567
+ case 'at': return ([l, i]) => { if (i >= l.length)
568
+ throw new Error(`at: index ${i} out of range`); return l[i]; };
569
+ case 'append': {
570
+ if (!isMultiset)
571
+ return ([a, b]) => a.concat(b);
572
+ const sort = canonicalSort(elemOf(types[0]));
573
+ return ([a, b]) => sort(a.concat(b));
574
+ }
575
+ case 'ofFn': return ([n, f]) => Array.from({ length: n }, (_, i) => f(i));
576
+ case 'show': {
577
+ const s = show(types[0]);
578
+ return ([x]) => s(x);
579
+ }
580
+ case 'concat': return xs => xs.join('');
581
+ case 'agents': return ([C]) => agentIndices(C);
582
+ case 'nodes': return () => Array.from({ length: rt.topo.size }, (_, i) => i);
583
+ case 'size': return () => rt.topo.size;
584
+ case 'decisionOf': return ([a]) => rt.decisions[a];
585
+ case 'target': return ([n, m]) => rt.topo.target(n, m);
586
+ case 'advance': return ([n, m, i]) => {
587
+ const N = rt.topo.size;
588
+ return m === 'cw' ? (n + i) % N : m === 'ccw' ? (n + N - i % N) % N : n;
589
+ };
590
+ case 'edgePresent': return ([n, m]) => rt.topo.canMove(rt.topoState, n, m);
591
+ case 'offset': return ([n, f, i]) => rt.topo.offset(n, f, i);
592
+ case 'toGlobal': return ([f, m]) => rt.topo.toGlobal(f, m);
593
+ case 'opposite': return ([m]) => opposite(m);
594
+ case 'frames': return () => rt.topo.frames;
595
+ case 'node': return ([n]) => n % rt.topo.size;
596
+ }
597
+ return this.fail(`builtin ${fn} is not implemented`);
598
+ }
599
+ }
600
+ /** Lexicographic order on lists of Bool or Nat (`false < true`, a prefix first), as Lean's `List.lt`. */
601
+ function lexCompare(a, b) {
602
+ const n = Math.min(a.length, b.length);
603
+ for (let i = 0; i < n; i++)
604
+ if (a[i] !== b[i])
605
+ return a[i] < b[i] ? -1 : 1;
606
+ return a.length - b.length;
607
+ }
608
+ function agentIndices(C) {
609
+ return C.idx ??= C.agents.map((_, i) => i);
610
+ }
611
+ function indexMarks(idx) {
612
+ const marks = [];
613
+ for (const i of idx)
614
+ marks[i] = true;
615
+ return marks;
616
+ }
617
+ /** Sub-collections with at most `k` elements, smallest first, then in index order. */
618
+ function subsetsOf(l, k, t) {
619
+ const out = [];
620
+ const pick = (size, from, current) => {
621
+ if (current.length === size) {
622
+ out.push(current.slice());
623
+ return;
624
+ }
625
+ for (let i = from; i < l.length; i++) {
626
+ current.push(l[i]);
627
+ pick(size, i + 1, current);
628
+ current.pop();
629
+ }
630
+ };
631
+ for (let size = 0; size <= Math.min(k, l.length); size++)
632
+ pick(size, 0, []);
633
+ if (t.k !== 'multiset')
634
+ return out;
635
+ // Equal elements of a multiset give equal sub-multisets: keep one of each.
636
+ const enc = encoder(t);
637
+ const seen = new Set();
638
+ return out.filter(s => { const e = enc(s); if (seen.has(e))
639
+ return false; seen.add(e); return true; });
640
+ }
@@ -0,0 +1,21 @@
1
+ import { Explorer } from "./explorer.js";
2
+ import { System } from "./system.js";
3
+ export function conformanceGraph(name, experiment, params, maxStates = 40) {
4
+ const system = new System(experiment, params);
5
+ const explorer = new Explorer(system, { symmetry: false, maxStates });
6
+ explorer.run();
7
+ const ids = new Map(explorer.states.map(s => [s.key, s.id]));
8
+ const edges = [];
9
+ for (const s of explorer.states) {
10
+ if (!s.succ)
11
+ continue;
12
+ for (const t of system.successors(s.config)) {
13
+ edges.push({ from: s.id, to: ids.get(t.key), witness: t.witness.map(w => w.label), activated: [...t.activated].sort() });
14
+ }
15
+ }
16
+ return {
17
+ name, experiment, params, maxStates,
18
+ states: explorer.states.map(s => ({ config: { agents: s.config.agents, world: s.config.world }, verdict: s.verdict })),
19
+ edges,
20
+ };
21
+ }