@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.
- package/LICENCE.txt +515 -0
- package/dist/check.js +162 -0
- package/dist/compile.js +640 -0
- package/dist/conformance.js +21 -0
- package/dist/explorer.js +303 -0
- package/dist/symmetry.js +111 -0
- package/dist/system.js +587 -0
- package/dist/topology.js +82 -0
- package/dist/values.js +129 -0
- package/package.json +27 -3
- package/src/check.ts +210 -0
- package/src/compile.ts +596 -0
- package/src/conformance.ts +38 -0
- package/src/explorer.ts +330 -0
- package/src/symmetry.ts +119 -0
- package/src/system.ts +622 -0
- package/src/topology.ts +120 -0
- package/src/values.ts +148 -0
- package/README.md +0 -3
package/src/system.ts
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An experiment with concrete parameters: its initial configurations, the
|
|
3
|
+
* successors of a configuration under the model's round, and the task's
|
|
4
|
+
* verdict (plan, Sections 3.7 and 6.4).
|
|
5
|
+
*/
|
|
6
|
+
import { check } from '@bramas/simulant-lang/check';
|
|
7
|
+
import type { Component, Def, Experiment, Type } from '@bramas/simulant-lang/ir';
|
|
8
|
+
import type { Layout } from '@bramas/simulant-lang/layout';
|
|
9
|
+
import { T } from '@bramas/simulant-lang/types';
|
|
10
|
+
import {
|
|
11
|
+
Compiler, Runtime, type AgentV, type Choice, type Config, type DecisionV, type Effect, type RootFn, type TraceCall,
|
|
12
|
+
} from './compile';
|
|
13
|
+
import { DynamicRing, Ring, type Move, type TopologyRuntime } from './topology';
|
|
14
|
+
import { encoder, show, type V } from './values';
|
|
15
|
+
|
|
16
|
+
export type { AgentV, Choice, Config, DecisionV, TraceCall };
|
|
17
|
+
|
|
18
|
+
/** What an activated agent saw in a step: its frame and its view. */
|
|
19
|
+
export type StepInput = { frame: V; view: Record<string, V> };
|
|
20
|
+
|
|
21
|
+
/** Why an agent took its decision in a step (`System.explain`). */
|
|
22
|
+
export type Explanation = {
|
|
23
|
+
/** The source line of the statement that decided (`Expr.at`), when the algorithm has lines. */
|
|
24
|
+
line?: number;
|
|
25
|
+
frame: string;
|
|
26
|
+
/** Each sensor's value, as text. */
|
|
27
|
+
view: Record<string, string>;
|
|
28
|
+
/** The local move, as the algorithm returned it. */
|
|
29
|
+
move: string;
|
|
30
|
+
/** The definitions the step called, with their arguments, results and return lines. */
|
|
31
|
+
calls: TraceCall[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type Successor = {
|
|
35
|
+
config: Config;
|
|
36
|
+
key: string;
|
|
37
|
+
/** The choices of the first (smallest) witness leading to this configuration. */
|
|
38
|
+
witness: Choice[];
|
|
39
|
+
/** Agents (indices in the source configuration) activated by some witness of this step. */
|
|
40
|
+
activated: Set<number>;
|
|
41
|
+
/** When recording (`explain`): each agent's inputs (by index in the source configuration), and the source index of each agent of `config`. */
|
|
42
|
+
inputs?: (StepInput | undefined)[];
|
|
43
|
+
order?: number[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type Verdict = 'success' | 'failure' | 'inProgress';
|
|
47
|
+
|
|
48
|
+
/** Interpreter-only algorithms (`Algorithm.native`). */
|
|
49
|
+
export type NativeStep = (mem: V, pub: V, view: Record<string, V>) => { move: Move; pub?: V; mem?: V; [action: string]: V };
|
|
50
|
+
|
|
51
|
+
export type SystemOptions = {
|
|
52
|
+
natives?: Record<string, NativeStep>;
|
|
53
|
+
/** Store anonymous agents sorted (default: under FSYNC). Off, agents keep their index, as in Lean. */
|
|
54
|
+
sortAgents?: boolean;
|
|
55
|
+
/** Remember the step's result for each input (default: on). */
|
|
56
|
+
memoStep?: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export class IRError extends Error {
|
|
60
|
+
constructor(readonly errors: string[]) {
|
|
61
|
+
super(errors.join('\n'));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function frameOf(fn: RootFn, args: V[]): V[] {
|
|
66
|
+
const fr = new Array(fn.nslots);
|
|
67
|
+
for (let i = 0; i < args.length; i++) fr[i] = args[i];
|
|
68
|
+
return fr;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function runDet(fn: RootFn, args: V[]): V {
|
|
72
|
+
if (!fn.det) throw new Error('expected a deterministic expression');
|
|
73
|
+
return fn.det(frameOf(fn, args));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function runND(fn: RootFn, args: V[], k: (v: V) => void): void {
|
|
77
|
+
const fr = frameOf(fn, args);
|
|
78
|
+
if (fn.det) k(fn.det(fr));
|
|
79
|
+
else fn.nd!(fr, k);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function makeTopology(exp: Experiment, size: number): TopologyRuntime {
|
|
83
|
+
const t = exp.model.topology;
|
|
84
|
+
return t.kind === 'ring' ? new Ring(size) : new DynamicRing(size, t.k);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const MOVES: Move[] = ['stay', 'cw', 'ccw'];
|
|
88
|
+
|
|
89
|
+
export class System {
|
|
90
|
+
readonly rt: Runtime;
|
|
91
|
+
readonly lay: Layout;
|
|
92
|
+
readonly topo: TopologyRuntime;
|
|
93
|
+
/** Anonymous agents are stored as a sorted multiset; under SSYNC (or when asked) they keep their identity. */
|
|
94
|
+
readonly anonymous: boolean;
|
|
95
|
+
private readonly compiler: Compiler;
|
|
96
|
+
/** Compiled roots, with the values of their component's parameters (passed after the root's own variables). */
|
|
97
|
+
private readonly sensors: Bound[];
|
|
98
|
+
private readonly sensorNames: string[];
|
|
99
|
+
private readonly hooks: Record<'beforeLook' | 'beforeMove' | 'afterMove', Bound[]>;
|
|
100
|
+
private readonly step: (mem: V, pub: V, view: Record<string, V>) => Record<string, V>;
|
|
101
|
+
private readonly verdictFn: RootFn;
|
|
102
|
+
private readonly lineFn?: RootFn;
|
|
103
|
+
private readonly agentKey: (a: AgentV) => string;
|
|
104
|
+
private readonly worldKey: (w: Record<string, V>) => string;
|
|
105
|
+
private readonly decisionKey: (d: DecisionV) => string;
|
|
106
|
+
private readonly actionDefaults: Record<string, V>;
|
|
107
|
+
private readonly showFrame: (f: V) => string;
|
|
108
|
+
/** While set, successors record their agents' inputs (for `explain`). */
|
|
109
|
+
private recording = false;
|
|
110
|
+
private traced?: (mem: V, pub: V, view: Record<string, V>) => Record<string, V>;
|
|
111
|
+
|
|
112
|
+
constructor(readonly exp: Experiment, readonly params: Record<string, V>, opts: SystemOptions = {}) {
|
|
113
|
+
const checked = check(exp);
|
|
114
|
+
if (checked.errors.length) throw new IRError(checked.errors);
|
|
115
|
+
const lay = this.lay = checked.layout;
|
|
116
|
+
const rt = this.rt = new Runtime(lay);
|
|
117
|
+
for (const p of exp.params) {
|
|
118
|
+
if (!(p.name in params)) throw new Error(`missing parameter ${p.name}`);
|
|
119
|
+
}
|
|
120
|
+
rt.params = params;
|
|
121
|
+
const paramTypes = new Map(exp.params.map(p => [p.name, p.type]));
|
|
122
|
+
const compiler = this.compiler = new Compiler(rt, lay, paramTypes);
|
|
123
|
+
const { model, algorithm, task } = exp;
|
|
124
|
+
const defsOf = (...lists: (Def[] | undefined)[]) => new Map(lists.flatMap(l => l ?? []).map(d => [d.name, d]));
|
|
125
|
+
|
|
126
|
+
this.topo = rt.topo = makeTopology(exp, runDet(compiler.root(model.topology.size, [], defsOf(model.defs)), []));
|
|
127
|
+
this.anonymous = opts.sortAgents ?? model.round === 'fsync';
|
|
128
|
+
|
|
129
|
+
const sensorVars: [string, Type][] = [['C', T.config], ['me', T.agent], ['frame', T.frame]];
|
|
130
|
+
const sensorDefs = new Map(lay.sensors.map(s => {
|
|
131
|
+
const owner = model.components.find(c => c.name === s.owner);
|
|
132
|
+
const [vars, args] = componentParams(owner);
|
|
133
|
+
return [s.name, { fn: compiler.root(s.body, [...sensorVars, ...vars], defsOf(model.defs, owner?.defs)), args }];
|
|
134
|
+
}));
|
|
135
|
+
this.sensorNames = algorithm.uses;
|
|
136
|
+
this.sensors = algorithm.uses.map(name => sensorDefs.get(name)!);
|
|
137
|
+
|
|
138
|
+
this.hooks = { beforeLook: [], beforeMove: [], afterMove: [] };
|
|
139
|
+
for (const c of model.components) {
|
|
140
|
+
const [vars, args] = componentParams(c);
|
|
141
|
+
for (const hook of ['beforeLook', 'beforeMove', 'afterMove'] as const) {
|
|
142
|
+
const body = c[hook];
|
|
143
|
+
if (body) this.hooks[hook].push({ fn: compiler.root(body, [['C', T.config], ...vars], defsOf(model.defs, c.defs)), args });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.actionDefaults = Object.fromEntries(lay.actions.map(a => [a.name, a.default]));
|
|
148
|
+
if (algorithm.native) {
|
|
149
|
+
const native = opts.natives?.[algorithm.native];
|
|
150
|
+
if (!native) throw new Error(`native algorithm ${algorithm.native} is not registered`);
|
|
151
|
+
this.step = native;
|
|
152
|
+
} else {
|
|
153
|
+
const stepFn = compiler.root(algorithm.step, [['mem', algorithm.mem], ['pub', algorithm.pub], ['view', lay.view]], defsOf(model.defs, algorithm.defs));
|
|
154
|
+
const step = (mem: V, pub: V, view: Record<string, V>) => runDet(stepFn, [mem, pub, view]) as Record<string, V>;
|
|
155
|
+
if (opts.memoStep === false) this.step = step;
|
|
156
|
+
else {
|
|
157
|
+
// Steps are deterministic: an algorithm that computes much (views of a whole ring) runs once per input.
|
|
158
|
+
const [memKey, pubKey, viewKey] = [encoder(algorithm.mem), encoder(lay.pub), encoder(lay.view)];
|
|
159
|
+
const memo = new Map<string, Record<string, V>>();
|
|
160
|
+
this.step = (mem, pub, view) => {
|
|
161
|
+
const key = `${memKey(mem)}|${pubKey(pub)}|${viewKey(view)}`;
|
|
162
|
+
let out = memo.get(key);
|
|
163
|
+
if (!out) {
|
|
164
|
+
if (memo.size >= 1 << 17) memo.clear();
|
|
165
|
+
out = step(mem, pub, view);
|
|
166
|
+
memo.set(key, out);
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (algorithm.line) this.lineFn = compiler.root(algorithm.line, [['mem', algorithm.mem]], defsOf(model.defs, algorithm.defs));
|
|
173
|
+
this.verdictFn = compiler.root(task.verdict, [['C', T.config]], defsOf(model.defs, task.defs));
|
|
174
|
+
|
|
175
|
+
const pos = encoder(T.node);
|
|
176
|
+
const fields = [
|
|
177
|
+
...lay.flags.map(f => (a: AgentV) => a.f[f.name] ? '1' : '0'),
|
|
178
|
+
...lay.agentFields.map(f => { const e = encoder(f.type); return (a: AgentV) => e(a.f[f.name]); }),
|
|
179
|
+
];
|
|
180
|
+
const pubKey = encoder(lay.pub);
|
|
181
|
+
const memKey = encoder(lay.mem);
|
|
182
|
+
this.agentKey = a => {
|
|
183
|
+
let s = pos(a.pos) + '|';
|
|
184
|
+
for (const f of fields) s += f(a);
|
|
185
|
+
return s + '|' + pubKey(a.pub) + '|' + memKey(a.mem);
|
|
186
|
+
};
|
|
187
|
+
this.worldKey = encoder(lay.worldType);
|
|
188
|
+
this.decisionKey = encoder(lay.decision);
|
|
189
|
+
this.showFrame = show(T.frame);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// -------------------------------------------------------------------------
|
|
193
|
+
// Configurations
|
|
194
|
+
|
|
195
|
+
key(C: Config): string {
|
|
196
|
+
let s = '';
|
|
197
|
+
for (const a of C.agents) s += this.agentKey(a) + ';';
|
|
198
|
+
return s + '#' + this.worldKey(C.world);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Sorts anonymous agents, so that no order leaks into state equality. */
|
|
202
|
+
canonical(C: Config): Config {
|
|
203
|
+
return this.canonicalOrder(C).config;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** `canonical`, with the index in `C` of each agent of the result. */
|
|
207
|
+
private canonicalOrder(C: Config): { config: Config; order: number[] } {
|
|
208
|
+
const identity = C.agents.map((_, i) => i);
|
|
209
|
+
if (!this.anonymous || C.agents.length < 2) return { config: C, order: identity };
|
|
210
|
+
const keyed = C.agents.map((a, i) => [this.agentKey(a), a, i] as const);
|
|
211
|
+
keyed.sort((x, y) => x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0);
|
|
212
|
+
return { config: { agents: keyed.map(([, a]) => a), world: C.world }, order: keyed.map(([, , i]) => i) };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Why the agent at `index` in the successor of `C` with key `key` took its decision (see `explainAll`). */
|
|
216
|
+
explain(C: Config, key: string, index: number): Explanation | undefined {
|
|
217
|
+
return this.explainAll(C, key)[index];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Why each agent of the successor of `C` with key `key` (by index in it) took its decision: the
|
|
222
|
+
* step is replayed with the same inputs, by a traced compilation of the algorithm. Undefined for
|
|
223
|
+
* agents that were not activated (or did not decide) in that step.
|
|
224
|
+
*/
|
|
225
|
+
explainAll(C: Config, key: string): (Explanation | undefined)[] {
|
|
226
|
+
this.recording = true;
|
|
227
|
+
let succ: Successor | undefined;
|
|
228
|
+
try {
|
|
229
|
+
succ = this.successors(C).find(s => s.key === key);
|
|
230
|
+
} finally {
|
|
231
|
+
this.recording = false;
|
|
232
|
+
}
|
|
233
|
+
if (!succ?.order || !succ.inputs) return [];
|
|
234
|
+
return succ.order.map(from => {
|
|
235
|
+
const input = succ!.inputs![from];
|
|
236
|
+
return input && this.explainStep(C.agents[from], input);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private explainStep(a: AgentV, input: StepInput): Explanation {
|
|
241
|
+
const { rt, exp, lay } = this;
|
|
242
|
+
const view = Object.fromEntries(lay.view.k === 'record'
|
|
243
|
+
? lay.view.fields.map(f => [f.name, show(f.type, true)(input.view[f.name])])
|
|
244
|
+
: Object.entries(input.view).map(([k, v]) => [k, String(v)]));
|
|
245
|
+
if (exp.algorithm.native) {
|
|
246
|
+
const local = this.step(a.mem, a.pub, input.view);
|
|
247
|
+
return { frame: this.showFrame(input.frame), view, move: String(local.move), calls: [] };
|
|
248
|
+
}
|
|
249
|
+
if (!this.traced) {
|
|
250
|
+
const { model, algorithm } = exp;
|
|
251
|
+
const compiler = new Compiler(rt, lay, new Map(exp.params.map(p => [p.name, p.type])), true);
|
|
252
|
+
const defs = new Map([...(model.defs ?? []), ...(algorithm.defs ?? [])].map(d => [d.name, d]));
|
|
253
|
+
const fn = compiler.root(algorithm.step, [['mem', algorithm.mem], ['pub', algorithm.pub], ['view', lay.view]], defs);
|
|
254
|
+
this.traced = (mem, pub, v) => runDet(fn, [mem, pub, v]) as Record<string, V>;
|
|
255
|
+
}
|
|
256
|
+
rt.trace = { stack: [[]], count: 0 };
|
|
257
|
+
try {
|
|
258
|
+
const local = this.traced(a.mem, a.pub, input.view);
|
|
259
|
+
return { line: rt.trace.line, frame: this.showFrame(input.frame), view, move: String(local.move), calls: rt.trace.stack[0] };
|
|
260
|
+
} finally {
|
|
261
|
+
rt.trace = undefined;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
verdict(C: Config): Verdict {
|
|
266
|
+
return runDet(this.verdictFn, [C]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
line(a: AgentV): number | undefined {
|
|
270
|
+
return this.lineFn && runDet(this.lineFn, [a.mem]);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// -------------------------------------------------------------------------
|
|
274
|
+
// Initial configurations
|
|
275
|
+
|
|
276
|
+
/** The ring size under other parameter values. */
|
|
277
|
+
sizeWith(params: Record<string, V>): number {
|
|
278
|
+
const saved = this.rt.params;
|
|
279
|
+
this.rt.params = params;
|
|
280
|
+
try {
|
|
281
|
+
const defs = new Map((this.exp.model.defs ?? []).map(d => [d.name, d]));
|
|
282
|
+
return runDet(this.compiler.root(this.exp.model.topology.size, [], defs), []);
|
|
283
|
+
} finally {
|
|
284
|
+
this.rt.params = saved;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The initial configurations under other parameter values with the same ring size. Parameters
|
|
290
|
+
* reach only the initialisation and the topology size (the checker enforces it), so this system
|
|
291
|
+
* explores them exactly as `new System(exp, params)` would.
|
|
292
|
+
*/
|
|
293
|
+
initialWith(params: Record<string, V>): Successor[] {
|
|
294
|
+
if (this.sizeWith(params) !== this.topo.size) throw new Error('initialWith: another ring size needs another system');
|
|
295
|
+
const saved = this.rt.params;
|
|
296
|
+
this.rt.params = params;
|
|
297
|
+
try {
|
|
298
|
+
return this.initial();
|
|
299
|
+
} finally {
|
|
300
|
+
this.rt.params = saved;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
initial(): Successor[] {
|
|
305
|
+
const { exp, compiler, lay, rt } = this;
|
|
306
|
+
const defs = new Map((exp.model.defs ?? []).map(d => [d.name, d]));
|
|
307
|
+
const algDefs = new Map([...(exp.model.defs ?? []), ...(exp.algorithm.defs ?? [])].map(d => [d.name, d]));
|
|
308
|
+
const k: number = runDet(compiler.root(exp.agents.count, [], defs), []);
|
|
309
|
+
const posFn = compiler.root(exp.agents.pos, [['i', T.nat]], defs);
|
|
310
|
+
const pubFn = compiler.root(exp.algorithm.init.pub, [['i', T.nat]], algDefs);
|
|
311
|
+
const memFn = compiler.root(exp.algorithm.init.mem, [['i', T.nat]], algDefs);
|
|
312
|
+
const flags = Object.fromEntries(lay.flags.map(f => [f.name, false]));
|
|
313
|
+
const base: AgentV[] = Array.from({ length: k }, (_, i) => ({
|
|
314
|
+
pos: runDet(posFn, [i]) % this.topo.size, pub: runDet(pubFn, [i]), mem: runDet(memFn, [i]), f: { ...flags },
|
|
315
|
+
}));
|
|
316
|
+
|
|
317
|
+
// Choices of initial values: agent fields (per agent), then world fields.
|
|
318
|
+
type Slot = { fn: RootFn; args: V[]; set: (agents: AgentV[], world: Record<string, V>, v: V) => void; agent?: number };
|
|
319
|
+
const slots: Slot[] = [];
|
|
320
|
+
for (const c of exp.model.components) {
|
|
321
|
+
const cdefs = new Map([...(exp.model.defs ?? []), ...(c.defs ?? [])].map(d => [d.name, d]));
|
|
322
|
+
const [vars, args] = componentParams(c);
|
|
323
|
+
for (const f of c.agent ?? []) {
|
|
324
|
+
const fn = compiler.root(c.init!.agent![f.name], [['i', T.nat], ...vars], cdefs);
|
|
325
|
+
for (let i = 0; i < k; i++) slots.push({ fn, args: [i, ...args], agent: i, set: (agents, _w, v) => { agents[i].f[f.name] = v; } });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const c of exp.model.components) {
|
|
329
|
+
const cdefs = new Map([...(exp.model.defs ?? []), ...(c.defs ?? [])].map(d => [d.name, d]));
|
|
330
|
+
const [vars, args] = componentParams(c);
|
|
331
|
+
for (const f of c.world ?? []) {
|
|
332
|
+
const fn = compiler.root(c.init!.world![f.name], vars, cdefs);
|
|
333
|
+
slots.push({ fn, args, set: (_a, world, v) => { world[f.name] = v; } });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const out = new Map<string, Successor>();
|
|
338
|
+
const values: V[] = [];
|
|
339
|
+
rt.witness = [];
|
|
340
|
+
const go = (j: number) => {
|
|
341
|
+
if (j === slots.length) {
|
|
342
|
+
const agents = base.map(a => ({ ...a, f: { ...a.f } }));
|
|
343
|
+
const world: Record<string, V> = {};
|
|
344
|
+
slots.forEach((s, i) => s.set(agents, world, values[i]));
|
|
345
|
+
const config = this.canonical({ agents, world });
|
|
346
|
+
const key = this.key(config);
|
|
347
|
+
if (!out.has(key)) out.set(key, { config, key, witness: rt.witness.slice(), activated: new Set() });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const s = slots[j];
|
|
351
|
+
const depth = rt.witness.length;
|
|
352
|
+
runND(s.fn, s.args, v => {
|
|
353
|
+
values[j] = v;
|
|
354
|
+
if (s.agent !== undefined) for (let i = depth; i < rt.witness.length; i++) rt.witness[i] = prefix(rt.witness[i], s.agent);
|
|
355
|
+
go(j + 1);
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
go(0);
|
|
359
|
+
return [...out.values()];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// -------------------------------------------------------------------------
|
|
363
|
+
// The standard round (plan 3.7)
|
|
364
|
+
|
|
365
|
+
successors(C: Config): Successor[] {
|
|
366
|
+
const { rt, topo } = this;
|
|
367
|
+
const out = new Map<string, Successor>();
|
|
368
|
+
const states = topo.states();
|
|
369
|
+
rt.witness = [];
|
|
370
|
+
states.forEach(({ state, label }, si) => {
|
|
371
|
+
// 1. The topology state, then beforeLook.
|
|
372
|
+
rt.topoState = state;
|
|
373
|
+
if (states.length > 1) rt.witness.push({ site: 'topology', index: si, label });
|
|
374
|
+
this.parallel(C, this.hooks.beforeLook, eff => {
|
|
375
|
+
const C1 = this.apply(C, eff);
|
|
376
|
+
// 2. Activation.
|
|
377
|
+
this.activate(C1, active => {
|
|
378
|
+
// 3. Frames, look and compute.
|
|
379
|
+
this.decide(C1, active, (decisions, inputs) => {
|
|
380
|
+
rt.decisions = decisions;
|
|
381
|
+
// 4. beforeMove, with the decisions.
|
|
382
|
+
this.parallel(C1, this.hooks.beforeMove, eff2 => {
|
|
383
|
+
// 5. Move.
|
|
384
|
+
const C3 = this.move(this.apply(C1, eff2), decisions);
|
|
385
|
+
// 6. afterMove.
|
|
386
|
+
this.parallel(C3, this.hooks.afterMove, eff3 => {
|
|
387
|
+
const { config, order } = this.canonicalOrder(this.apply(C3, eff3));
|
|
388
|
+
const key = this.key(config);
|
|
389
|
+
const known = out.get(key);
|
|
390
|
+
if (known) for (const i of active) known.activated.add(i);
|
|
391
|
+
else out.set(key, {
|
|
392
|
+
config, key, witness: rt.witness.slice(), activated: new Set(active),
|
|
393
|
+
...(this.recording ? { inputs: inputs.slice(), order } : {}),
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
if (states.length > 1) rt.witness.pop();
|
|
401
|
+
});
|
|
402
|
+
return [...out.values()];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** All hooks of a hook point read the same snapshot; their effects are merged. */
|
|
406
|
+
private parallel(C: Config, hooks: Bound[], k: (eff: Effect | undefined) => void) {
|
|
407
|
+
if (!hooks.length) return k(undefined);
|
|
408
|
+
const effects: Effect[] = new Array(hooks.length);
|
|
409
|
+
const go = (i: number): void => {
|
|
410
|
+
if (i === hooks.length) return k(merge(effects));
|
|
411
|
+
runND(hooks[i].fn, [C, ...hooks[i].args], e => { effects[i] = e; go(i + 1); });
|
|
412
|
+
};
|
|
413
|
+
go(0);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private apply(C: Config, eff: Effect | undefined): Config {
|
|
417
|
+
if (!eff) return C;
|
|
418
|
+
const worldNames = Object.keys(eff.world);
|
|
419
|
+
const world = worldNames.length ? { ...C.world, ...eff.world } : C.world;
|
|
420
|
+
let agents = C.agents;
|
|
421
|
+
for (const [flag, marks] of Object.entries(eff.raise)) {
|
|
422
|
+
for (let i = 0; i < agents.length; i++) {
|
|
423
|
+
if (!marks[i] || agents[i].f[flag]) continue;
|
|
424
|
+
if (agents === C.agents) agents = agents.slice();
|
|
425
|
+
agents[i] = { ...agents[i], f: { ...agents[i].f, [flag]: true } };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return agents === C.agents && world === C.world ? C : { agents, world };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** FSYNC activates every agent that acts; SSYNC lets the adversary choose a nonempty set of them. */
|
|
432
|
+
private activate(C: Config, k: (active: number[]) => void) {
|
|
433
|
+
const eligible = C.agents.flatMap((a, i) => this.rt.acts(a) === 'never' ? [] : [i]);
|
|
434
|
+
if (this.exp.model.round === 'fsync' || eligible.length === 0) return k(eligible);
|
|
435
|
+
const w = this.rt.witness;
|
|
436
|
+
let index = 0;
|
|
437
|
+
for (let size = 1; size <= eligible.length; size++) {
|
|
438
|
+
const pick = (from: number, current: number[]) => {
|
|
439
|
+
if (current.length === size) {
|
|
440
|
+
w.push({ site: 'activation', index: index++, label: `activates ${current.map(i => `a${i}`).join(', ')}` });
|
|
441
|
+
k(current.slice());
|
|
442
|
+
w.pop();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
for (let i = from; i < eligible.length; i++) {
|
|
446
|
+
current.push(eligible[i]);
|
|
447
|
+
pick(i + 1, current);
|
|
448
|
+
current.pop();
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
pick(0, []);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private idle(a: AgentV): DecisionV {
|
|
456
|
+
return { decided: false, move: 'stay', pub: a.pub, mem: a.mem, ...this.actionDefaults };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** The product, over the activated agents, of their possible decisions (with the inputs of each, by agent). */
|
|
460
|
+
private decide(C: Config, active: number[], k: (decisions: DecisionV[], inputs: (StepInput | undefined)[]) => void) {
|
|
461
|
+
const options = active.map(i => this.options(C, i));
|
|
462
|
+
const decisions = C.agents.map(a => this.idle(a));
|
|
463
|
+
const inputs: (StepInput | undefined)[] = C.agents.map(() => undefined);
|
|
464
|
+
const w = this.rt.witness;
|
|
465
|
+
const go = (j: number): void => {
|
|
466
|
+
if (j === active.length) return k(decisions.slice(), inputs);
|
|
467
|
+
for (const { d, w: parts, input } of options[j]) {
|
|
468
|
+
decisions[active[j]] = d;
|
|
469
|
+
inputs[active[j]] = input;
|
|
470
|
+
for (const p of parts) w.push(p);
|
|
471
|
+
go(j + 1);
|
|
472
|
+
w.length -= parts.length;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
go(0);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private frameChoices(a: AgentV): V[] {
|
|
479
|
+
const fm = this.exp.model.frame;
|
|
480
|
+
if (fm.kind === 'common') return [this.topo.identityFrame];
|
|
481
|
+
if (fm.kind === 'dynamicReflection') return this.topo.frames;
|
|
482
|
+
return [a.f[fm.field]];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* The decisions an agent may take: one per frame and per outcome of its
|
|
487
|
+
* sensors' choices, deduplicated by their effect (global move, new state,
|
|
488
|
+
* actions), keeping the first witness.
|
|
489
|
+
*/
|
|
490
|
+
private options(C: Config, i: number): { d: DecisionV; w: Choice[]; input?: StepInput }[] {
|
|
491
|
+
const { rt } = this;
|
|
492
|
+
const a = C.agents[i];
|
|
493
|
+
if (rt.acts(a) === 'adversary') {
|
|
494
|
+
return MOVES.map((move, index) => ({
|
|
495
|
+
d: { ...this.idle(a), decided: true, move },
|
|
496
|
+
w: [{ site: `adversary a${i}`, index, label: `a${i} (adversary): ${move}` }],
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
const saved = rt.witness;
|
|
500
|
+
const found = new Map<string, { d: DecisionV; w: Choice[]; input?: StepInput }>();
|
|
501
|
+
const frames = this.frameChoices(a);
|
|
502
|
+
frames.forEach((frame, fi) => {
|
|
503
|
+
rt.witness = frames.length > 1 ? [{ site: `frame a${i}`, index: fi, label: `a${i}: ${this.showFrame(frame)}` }] : [];
|
|
504
|
+
const depth = rt.witness.length;
|
|
505
|
+
this.view(C, i, frame, view => {
|
|
506
|
+
const local = this.step(a.mem, a.pub, view);
|
|
507
|
+
const d: DecisionV = {
|
|
508
|
+
...this.actionDefaults, ...local, decided: true,
|
|
509
|
+
move: this.topo.toGlobal(frame, local.move as Move), pub: local.pub ?? a.pub, mem: local.mem ?? a.mem,
|
|
510
|
+
};
|
|
511
|
+
const key = this.decisionKey(d);
|
|
512
|
+
if (!found.has(key)) {
|
|
513
|
+
found.set(key, { d, w: rt.witness.map((c, j) => j < depth ? c : prefix(c, i)), ...(this.recording ? { input: { frame, view } } : {}) });
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
rt.witness = saved;
|
|
518
|
+
return [...found.values()];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** The sensors of an activation, evaluated once each (with their choices). */
|
|
522
|
+
private view(C: Config, i: number, frame: V, k: (view: Record<string, V>) => void) {
|
|
523
|
+
const { sensors, sensorNames } = this;
|
|
524
|
+
const view: Record<string, V> = {};
|
|
525
|
+
const go = (j: number): void => {
|
|
526
|
+
if (j === sensors.length) return k({ ...view });
|
|
527
|
+
runND(sensors[j].fn, [C, i, frame, ...sensors[j].args], v => { view[sensorNames[j]] = v; go(j + 1); });
|
|
528
|
+
};
|
|
529
|
+
go(0);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Rigid movement along the edges present in this round; only agents that still act apply their decision. */
|
|
533
|
+
private move(C: Config, decisions: DecisionV[]): Config {
|
|
534
|
+
const { rt, topo } = this;
|
|
535
|
+
let changed = false;
|
|
536
|
+
const agents = C.agents.map((a, i) => {
|
|
537
|
+
const d = decisions[i];
|
|
538
|
+
if (!d.decided || rt.acts(a) === 'never') return a;
|
|
539
|
+
const pos = d.move !== 'stay' && topo.canMove(rt.topoState, a.pos, d.move) ? topo.target(a.pos, d.move) : a.pos;
|
|
540
|
+
if (pos === a.pos && d.pub === a.pub && d.mem === a.mem) return a;
|
|
541
|
+
changed = true;
|
|
542
|
+
return { ...a, pos, pub: d.pub, mem: d.mem };
|
|
543
|
+
});
|
|
544
|
+
return changed ? { agents, world: C.world } : C;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
type Bound = { fn: RootFn; args: V[] };
|
|
549
|
+
|
|
550
|
+
/** A component's parameters, as extra root variables, and their values. */
|
|
551
|
+
function componentParams(c: Component | undefined): [[string, Type][], V[]] {
|
|
552
|
+
const params = c?.params ?? [];
|
|
553
|
+
return [params.map(p => [p.name, p.type]), params.map(p => c!.args![p.name])];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function prefix(c: Choice, agent: number): Choice {
|
|
557
|
+
return { ...c, label: `a${agent}: ${c.label}` };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function merge(effects: Effect[]): Effect {
|
|
561
|
+
if (effects.length === 1) return effects[0];
|
|
562
|
+
const eff: Effect = { world: {}, raise: {} };
|
|
563
|
+
for (const e of effects) {
|
|
564
|
+
Object.assign(eff.world, e.world);
|
|
565
|
+
for (const [flag, marks] of Object.entries(e.raise)) {
|
|
566
|
+
const into = eff.raise[flag] ??= [];
|
|
567
|
+
marks.forEach((m, i) => { if (m) into[i] = true; });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return eff;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* The values of the experiment's parameters, for `check-all`: each domain
|
|
575
|
+
* is evaluated with the earlier parameters fixed.
|
|
576
|
+
*/
|
|
577
|
+
export function* parameterValues(exp: Experiment): Generator<Record<string, V>> {
|
|
578
|
+
const checked = check(exp);
|
|
579
|
+
if (checked.errors.length) throw new IRError(checked.errors);
|
|
580
|
+
const rt = new Runtime(checked.layout);
|
|
581
|
+
const compiler = new Compiler(rt, checked.layout, new Map(exp.params.map(p => [p.name, p.type])));
|
|
582
|
+
const defs = new Map((exp.model.defs ?? []).map(d => [d.name, d]));
|
|
583
|
+
const size = compiler.root(exp.model.topology.size, [], defs);
|
|
584
|
+
const domains = exp.params.map(p => p.domain && compiler.root(p.domain, [], defs));
|
|
585
|
+
function* go(i: number, params: Record<string, V>): Generator<Record<string, V>> {
|
|
586
|
+
if (i === exp.params.length) { yield { ...params }; return; }
|
|
587
|
+
const domain = domains[i];
|
|
588
|
+
if (!domain) throw new Error(`parameter ${exp.params[i].name} has no domain`);
|
|
589
|
+
rt.params = params;
|
|
590
|
+
try {
|
|
591
|
+
rt.topo = makeTopology(exp, runDet(size, []));
|
|
592
|
+
} catch {
|
|
593
|
+
// The size depends on a later parameter: domains cannot use the topology yet.
|
|
594
|
+
}
|
|
595
|
+
for (const v of runDet(domain, []) as V[]) yield* go(i + 1, { ...params, [exp.params[i].name]: v });
|
|
596
|
+
}
|
|
597
|
+
yield* go(0, {});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const definitionCache = new WeakMap<Experiment, Map<string, (...args: V[]) => V>>();
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* A definition of an experiment (`def f (x : T) : R := e` in `.sim`), as a function on values:
|
|
604
|
+
* renderers and tests reuse the algorithm's own definitions. Deterministic definitions only.
|
|
605
|
+
*/
|
|
606
|
+
export function definition(exp: Experiment, name: string): (...args: V[]) => V {
|
|
607
|
+
let cache = definitionCache.get(exp);
|
|
608
|
+
if (!cache) definitionCache.set(exp, cache = new Map());
|
|
609
|
+
let f = cache.get(name);
|
|
610
|
+
if (!f) {
|
|
611
|
+
const checked = check(exp);
|
|
612
|
+
if (checked.errors.length) throw new IRError(checked.errors);
|
|
613
|
+
const compiler = new Compiler(new Runtime(checked.layout), checked.layout, new Map(exp.params.map(p => [p.name, p.type])));
|
|
614
|
+
const defs = new Map([...(exp.model.defs ?? []), ...(exp.algorithm.defs ?? []), ...(exp.task.defs ?? [])].map(d => [d.name, d]));
|
|
615
|
+
const def = defs.get(name);
|
|
616
|
+
if (!def) throw new Error(`${exp.name} has no definition ${name}`);
|
|
617
|
+
const fn = compiler.root(def.body, def.params.map(p => [p.name, p.type]), defs);
|
|
618
|
+
f = (...args: V[]) => runDet(fn, args);
|
|
619
|
+
cache.set(name, f);
|
|
620
|
+
}
|
|
621
|
+
return f;
|
|
622
|
+
}
|