@bramas/simulant-ui 0.0.0-stage → 0.1.1

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.
Files changed (53) hide show
  1. package/LICENCE.txt +515 -0
  2. package/dist/SimulatorApp.jsx +601 -0
  3. package/dist/components/AlgorithmViewer.css +270 -0
  4. package/dist/components/AlgorithmViewer.jsx +218 -0
  5. package/dist/components/BranchPicker.css +96 -0
  6. package/dist/components/BranchPicker.jsx +65 -0
  7. package/dist/components/ConfigGraphGenerator.css +129 -0
  8. package/dist/components/ConfigGraphGenerator.jsx +408 -0
  9. package/dist/components/GenericEditor.jsx +228 -0
  10. package/dist/components/LeanView.jsx +133 -0
  11. package/dist/components/ModelView.css +520 -0
  12. package/dist/components/ModelView.jsx +272 -0
  13. package/dist/components/SettingsLibrary.jsx +61 -0
  14. package/dist/components/StateInspector.css +136 -0
  15. package/dist/components/StateInspector.jsx +103 -0
  16. package/dist/components/highlight.js +99 -0
  17. package/dist/pico.min.css +4 -0
  18. package/dist/renderers/define.js +28 -0
  19. package/dist/renderers/ring.js +103 -0
  20. package/dist/sim/branches.js +28 -0
  21. package/dist/sim/describe.js +408 -0
  22. package/dist/sim/execution.js +42 -0
  23. package/dist/sim/project.js +349 -0
  24. package/dist/sim/snapshot.js +29 -0
  25. package/dist/sim/types.js +1 -0
  26. package/dist/simulator.css +593 -0
  27. package/package.json +50 -3
  28. package/src/SimulatorApp.tsx +661 -0
  29. package/src/components/AlgorithmViewer.css +270 -0
  30. package/src/components/AlgorithmViewer.tsx +256 -0
  31. package/src/components/BranchPicker.css +96 -0
  32. package/src/components/BranchPicker.tsx +90 -0
  33. package/src/components/ConfigGraphGenerator.css +129 -0
  34. package/src/components/ConfigGraphGenerator.tsx +448 -0
  35. package/src/components/GenericEditor.tsx +413 -0
  36. package/src/components/LeanView.tsx +162 -0
  37. package/src/components/ModelView.css +520 -0
  38. package/src/components/ModelView.tsx +317 -0
  39. package/src/components/SettingsLibrary.tsx +84 -0
  40. package/src/components/StateInspector.css +136 -0
  41. package/src/components/StateInspector.tsx +125 -0
  42. package/src/components/highlight.ts +100 -0
  43. package/src/pico.min.css +4 -0
  44. package/src/renderers/define.ts +54 -0
  45. package/src/renderers/ring.ts +112 -0
  46. package/src/sim/branches.ts +30 -0
  47. package/src/sim/describe.ts +416 -0
  48. package/src/sim/execution.ts +65 -0
  49. package/src/sim/project.ts +377 -0
  50. package/src/sim/snapshot.ts +60 -0
  51. package/src/sim/types.ts +28 -0
  52. package/src/simulator.css +593 -0
  53. package/README.md +0 -3
@@ -0,0 +1,408 @@
1
+ import { derivedGroup } from "@bramas/simulant-engine/symmetry";
2
+ import { parameterValues } from "@bramas/simulant-engine/system";
3
+ /** `std/models.sim` for the standard library; otherwise the file's name. */
4
+ export function displayPath(file) {
5
+ const std = /[\\/]stdlib[\\/]sim[\\/](.*)$/.exec(file);
6
+ return std ? `std/${std[1]}` : file.split(/[\\/]/).pop();
7
+ }
8
+ /**
9
+ * Lines `line` to `endLine` of a file, with the doc comment (`/-- ... -/`) and attributes
10
+ * (`@[...]`) written just before them: what a reader of the file sees as the declaration.
11
+ */
12
+ export function excerpt(text, line, endLine) {
13
+ const all = text.split('\n');
14
+ let from = line;
15
+ for (let i = line - 2; i >= 0;) {
16
+ const t = all[i].trimEnd();
17
+ if (t.startsWith('@[')) {
18
+ from = i + 1;
19
+ i--;
20
+ continue;
21
+ }
22
+ if (!t.endsWith('-/'))
23
+ break;
24
+ let start = i;
25
+ while (start >= 0 && !all[start].includes('/-'))
26
+ start--;
27
+ if (start < 0 || !all[start].trimStart().startsWith('/--'))
28
+ break;
29
+ from = start + 1;
30
+ i = start - 1;
31
+ }
32
+ const out = [];
33
+ for (let n = from; n <= endLine; n++)
34
+ out.push({ number: n, text: (all[n - 1] ?? '').trimEnd() });
35
+ while (out.length && !out[out.length - 1].text)
36
+ out.pop();
37
+ return out;
38
+ }
39
+ /** The text of a module comment (`/-! ... -/`) at the start of a file, in paragraphs. */
40
+ function about(text) {
41
+ const m = /^\s*\/-!([\s\S]*?)-\//.exec(text);
42
+ return m ? m[1].trim().split(/\n\s*\n/).map(p => p.replace(/\s*\n\s*/g, ' ')).join('\n\n') : undefined;
43
+ }
44
+ /** An item of a declaration (`param N : Nat := ...`, `agents := 4`): its value as written, and its line. */
45
+ function item(lines, name) {
46
+ const i = lines.findIndex(l => name.test(l.text));
47
+ if (i < 0)
48
+ return undefined;
49
+ const indent = (s) => s.length - s.trimStart().length;
50
+ const own = indent(lines[i].text);
51
+ const parts = [lines[i].text.slice(lines[i].text.indexOf(':=') + 2).trim()];
52
+ for (let j = i + 1; j < lines.length && lines[j].text.trim() && indent(lines[j].text) > own; j++)
53
+ parts.push(lines[j].text.trim());
54
+ return { text: parts.join(' ').replace(/\s+--.*$/, ''), line: lines[i].number };
55
+ }
56
+ export function showType(t) {
57
+ const inner = (x) => {
58
+ const s = showType(x);
59
+ return /\s/.test(s) && !s.startsWith('{') ? `(${s})` : s;
60
+ };
61
+ switch (t.k) {
62
+ case 'enum': return t.name;
63
+ case 'option': return `Option ${inner(t.of)}`;
64
+ case 'list': return `List ${inner(t.of)}`;
65
+ case 'multiset': return `Multiset ${inner(t.of)}`;
66
+ case 'record': return t.fields.length ? `{ ${t.fields.map(f => `${f.name} : ${showType(f.type)}`).join(', ')} }` : 'Unit';
67
+ case 'fn': return `(${t.params.map(showType).join(', ')}) → ${showType(t.ret)}`;
68
+ default: return t.k === 'pub' ? 'Pub' : t.k[0].toUpperCase() + t.k.slice(1);
69
+ }
70
+ }
71
+ function showValue(v, t) {
72
+ if (t?.k === 'enum' && typeof v === 'string')
73
+ return `.${v}`;
74
+ return JSON.stringify(v);
75
+ }
76
+ /** The adversary's choice sites in expressions: `choose` names each one. */
77
+ function choices(...exprs) {
78
+ const out = new Set();
79
+ const walk = (x) => {
80
+ if (!x || typeof x !== 'object')
81
+ return;
82
+ if (Array.isArray(x)) {
83
+ x.forEach(walk);
84
+ return;
85
+ }
86
+ const o = x;
87
+ if (o.e === 'choose')
88
+ out.add(o.site);
89
+ for (const [k, v] of Object.entries(o))
90
+ if (k !== 'type')
91
+ walk(v);
92
+ };
93
+ exprs.forEach(walk);
94
+ return [...out];
95
+ }
96
+ const componentName = (c) => c.args && Object.keys(c.args).length
97
+ ? `${c.name} (${Object.entries(c.args).map(([k, v]) => `${k} := ${showValue(v, c.params?.find(p => p.name === k)?.type)}`).join(', ')})`
98
+ : c.name;
99
+ const GOALS = {
100
+ eventually: {
101
+ formula: '◇ success',
102
+ text: 'every execution eventually reaches a configuration with verdict success, and none reaches failure',
103
+ },
104
+ eventuallyAlways: {
105
+ formula: '◇□ success',
106
+ text: 'every execution eventually stays forever in configurations with verdict success, and none reaches failure',
107
+ },
108
+ safety: { formula: '□ ¬failure', text: 'no execution reaches a configuration with verdict failure' },
109
+ };
110
+ const HOOKS = ['beforeLook', 'beforeMove', 'afterMove'];
111
+ /** Above this many definitions, the algorithm's facts give their number rather than their names. */
112
+ const MANY_DEFS = 6;
113
+ /** Counts the settings of `check-all`, up to `cap`. */
114
+ function countSettings(exp, cap = 10_000) {
115
+ let n = 0;
116
+ try {
117
+ for (const _ of parameterValues(exp))
118
+ if (++n >= cap)
119
+ return `more than ${cap}`;
120
+ }
121
+ catch {
122
+ return 'unknown (a domain cannot be enumerated)';
123
+ }
124
+ return String(n);
125
+ }
126
+ /** The source line of each program-counter value, from the lowered algorithm's line table. */
127
+ function lineTable(exp) {
128
+ const l = exp.algorithm.line;
129
+ if (l?.e !== 'match')
130
+ return {};
131
+ return Object.fromEntries(Object.entries(l.cases).flatMap(([pc, e]) => e.e === 'lit' && typeof e.value === 'number' ? [[pc, e.value]] : []));
132
+ }
133
+ export function describeExperiment(exp, sources, revision, lean) {
134
+ const { model, algorithm, task } = exp;
135
+ const key = (kind, name) => `${kind} ${name}`;
136
+ // The declarations as written. Without recorded uses (IR built otherwise), the algorithm alone.
137
+ const uses = exp.source?.uses ?? (algorithm.source ? [{ kind: 'algorithm', name: algorithm.name, ...algorithm.source }] : []);
138
+ const excerpts = uses
139
+ .filter(u => sources[u.file] !== undefined)
140
+ .map(u => ({ kind: u.kind, name: u.name, file: u.file, lines: excerpt(sources[u.file], u.line, u.endLine) }));
141
+ const expLines = excerpts.find(s => s.kind === 'experiment')?.lines ?? [];
142
+ /** A declaration shown, by kind and name (a component's declaration is `Hazards.BlackHole`, its IR name `BlackHole`). */
143
+ const shown = (kind, name) => excerpts.find(s => s.kind === kind && (s.name === name || s.name.split('.').pop() === name));
144
+ const refTo = (kind, name, line) => {
145
+ const s = shown(kind, name);
146
+ return s ? [{ source: key(s.kind, s.name), ...(line !== undefined ? { line } : {}) }] : [];
147
+ };
148
+ const expItem = (name) => item(expLines, name);
149
+ const expRef = (i) => refTo('experiment', exp.name, i?.line);
150
+ /** Where a field of the model is set: the first model definition of the chain that sets it, or the model itself. */
151
+ const modelRef = (field) => {
152
+ const models = excerpts.filter(s => s.kind === 'model');
153
+ for (const m of models) {
154
+ const i = item(m.lines, new RegExp(`^\\s+${field}\\s*:=`));
155
+ if (i)
156
+ return [{ source: key(m.kind, m.name), line: i.line }];
157
+ }
158
+ return models.slice(0, 1).map(m => ({ source: key(m.kind, m.name) }));
159
+ };
160
+ const sensors = [
161
+ ...model.sensors.map(s => ({ s, owner: undefined })),
162
+ ...model.components.flatMap(c => (c.sensors ?? []).map(s => ({ s, owner: c.name }))),
163
+ ];
164
+ const actions = model.components.flatMap(c => c.actions ?? []);
165
+ const flags = model.components.flatMap(c => c.flags ?? []);
166
+ const size = model.topology.size.e === 'param' ? model.topology.size.name : 'the size expression';
167
+ const group = derivedGroup(exp);
168
+ const symmetry = group.rotations ? (group.reflections ? 'rotations and reflections' : 'rotations') : 'none';
169
+ const frame = {
170
+ common: { value: 'Common chirality', note: 'All agents share the orientation of the ring: a local cw is the same direction for every agent.' },
171
+ dynamicReflection: { value: 'Chosen at each activation', note: "The adversary chooses, at each activation, whether the agent's view is mirrored." },
172
+ persistent: { value: 'Fixed per agent', note: "Each agent's orientation is chosen at initialisation and kept for the whole execution." },
173
+ }[model.frame.kind];
174
+ const modelFacts = [
175
+ {
176
+ label: 'Topology',
177
+ value: model.topology.kind === 'ring' ? `Ring of ${size} nodes` : `Dynamic ring of ${size} nodes`,
178
+ note: model.topology.kind === 'ring'
179
+ ? 'Every edge is present in every round.'
180
+ : `In every round, the adversary removes up to ${model.topology.k} edge${model.topology.k === 1 ? '' : 's'}; missing edges are chosen anew each round and forgotten afterwards.`,
181
+ refs: modelRef('topology'),
182
+ },
183
+ {
184
+ label: 'Synchrony',
185
+ value: model.round === 'fsync' ? 'FSYNC (fully synchronous)' : model.round === 'sequential' ? 'Sequential (fair)' : 'SSYNC (semi-synchronous)',
186
+ note: model.round === 'fsync'
187
+ ? 'Every agent able to act is activated in every round.'
188
+ : (model.round === 'sequential'
189
+ ? 'In every round, the adversary activates exactly one agent able to act (or none if no agent can act). '
190
+ : 'In every round, the adversary activates a nonempty set of the agents able to act. ')
191
+ + 'Only fair executions count: every agent that keeps acting is activated infinitely often.',
192
+ refs: modelRef('round'),
193
+ },
194
+ {
195
+ label: 'Memory',
196
+ value: model.memory === 'persistent' ? 'Persistent' : 'Oblivious',
197
+ note: model.memory === 'persistent'
198
+ ? 'Agents keep a program counter and their local values between activations.'
199
+ : 'No program counter or local value is kept between activations; storage supplied by components (read by sensors, written by actions) is separate.',
200
+ refs: modelRef('memory'),
201
+ },
202
+ { label: 'Orientation', value: frame.value, note: frame.note + (model.frame.kind === 'persistent' ? ` (agent field ${model.frame.field})` : ''), refs: modelRef('frame') },
203
+ { label: 'Movement', value: 'Rigid', note: 'A move follows the edge to the adjacent node within the round, when that edge is present.' },
204
+ {
205
+ label: 'Sensors',
206
+ value: sensors.length ? sensors.map(x => x.s.name).join(', ') : 'none',
207
+ note: 'What algorithms can know of the configuration: each activated agent evaluates each sensor once per activation.',
208
+ details: sensors.map(({ s, owner }) => `${s.name} : ${showType(s.type)}${owner ? ` (from ${owner})` : ''}${algorithm.uses.includes(s.name) ? '' : ' — not used by the algorithm'}`),
209
+ refs: [...modelRef('sensors'), ...sensors.flatMap(x => refTo('sensor', x.s.name))],
210
+ },
211
+ ];
212
+ const componentFacts = model.components.map(c => {
213
+ const details = [];
214
+ if (c.world?.length)
215
+ details.push(`world: ${c.world.map(f => `${f.name} : ${showType(f.type)}`).join(', ')}`);
216
+ if (c.agent?.length)
217
+ details.push(`per agent: ${c.agent.map(f => `${f.name} : ${showType(f.type)}`).join(', ')}`);
218
+ for (const f of c.flags ?? []) {
219
+ details.push(`flag ${f.name}: ${f.visibility === 'hidden' ? 'the agent disappears from every sensor' : 'visible'}, ${f.acts === 'never' ? 'it no longer decides or moves' : 'the adversary decides its moves'}`);
220
+ }
221
+ for (const a of c.actions ?? [])
222
+ details.push(`action ${a.name} : ${showType(a.type)} (default ${showValue(a.default, a.type)})`);
223
+ const hooks = HOOKS.filter(h => c[h]);
224
+ if (hooks.length)
225
+ details.push(`acts in ${hooks.join(', ')}`);
226
+ const chosen = choices(c.init?.world && Object.values(c.init.world), c.init?.agent && Object.values(c.init.agent), ...HOOKS.map(h => c[h]));
227
+ if (chosen.length)
228
+ details.push(`adversary chooses: ${chosen.join(', ')}`);
229
+ return { label: c.name, value: componentName(c), details, refs: refTo('component', c.name) };
230
+ });
231
+ const params = exp.params.map(p => {
232
+ const i = expItem(new RegExp(`^\\s+param\\s+${p.name}\\b`));
233
+ return {
234
+ label: `param ${p.name}`,
235
+ value: p.domain ? i?.text ?? 'a domain' : 'no domain (set it in the settings)',
236
+ code: !!p.domain,
237
+ note: `of type ${showType(p.type)}`,
238
+ refs: expRef(i),
239
+ };
240
+ });
241
+ const agents = expItem(/^\s+agents\b/);
242
+ const position = expItem(/^\s+position\b/);
243
+ const limits = expItem(/^\s+limits\b/);
244
+ const sections = [
245
+ {
246
+ title: 'Experiment',
247
+ facts: [
248
+ { label: 'Agents', value: agents?.text ?? 'see the experiment', code: true, refs: expRef(agents) },
249
+ { label: 'Positions', value: position?.text ?? 'see the experiment', code: true, note: 'Initial position of agent i.', refs: expRef(position) },
250
+ ...params,
251
+ { label: 'Settings checked', value: countSettings(exp), note: 'Parameter values enumerated by check-all, from the domains above.' },
252
+ {
253
+ label: 'Limits',
254
+ value: exp.limits ? Object.entries(exp.limits).map(([k, v]) => `${k} ${v}`).join(', ') : 'none',
255
+ note: exp.limits ? 'A check that reaches a limit is incomplete unless it found a counterexample.' : 'Checks explore every reachable configuration.',
256
+ refs: exp.limits ? expRef(limits) : [],
257
+ },
258
+ ],
259
+ },
260
+ { title: `Model ${model.name}`, facts: modelFacts },
261
+ ...(componentFacts.length ? [{ title: 'Components', facts: componentFacts }] : []),
262
+ {
263
+ title: 'Algorithm',
264
+ facts: [
265
+ { label: 'Name', value: algorithm.name, note: algorithm.native ? `native TypeScript step ${algorithm.native}` : undefined, refs: refTo('algorithm', algorithm.name) },
266
+ { label: 'Reads', value: algorithm.uses.length ? algorithm.uses.join(', ') : 'no sensor' },
267
+ { label: 'Public state', value: showType(algorithm.pub), code: true, note: 'Visible to other agents through sensors.' },
268
+ {
269
+ label: 'Memory',
270
+ value: showType(algorithm.mem),
271
+ code: true,
272
+ note: algorithm.source?.calls !== undefined || algorithm.mem.k === 'record' ? 'Derived from the algorithm: its program counter and the locals kept across activations.' : undefined,
273
+ },
274
+ // A few definitions by name; many, by number (they are listed, folded, beside the declarations).
275
+ ...(algorithm.defs?.length ? [algorithm.defs.length <= MANY_DEFS
276
+ ? { label: 'Definitions', value: algorithm.defs.map(d => d.name).join(', '), refs: algorithm.defs.flatMap(d => refTo('definition', d.name)) }
277
+ : { label: 'Definitions', value: `${algorithm.defs.length} definitions`, note: 'Listed under Definitions, beside the declarations; a click on a name in the algorithm opens its definition.' }] : []),
278
+ ],
279
+ },
280
+ {
281
+ title: 'Task',
282
+ facts: [
283
+ { label: 'Verdict', value: task.name, note: 'Each configuration is judged success, failure or not yet.', refs: refTo('task', task.name) },
284
+ {
285
+ label: 'Goal',
286
+ value: GOALS[task.goal].formula,
287
+ note: `${model.round !== 'fsync' ? 'Over every fair execution' : 'Over every execution'}: ${GOALS[task.goal].text}.`,
288
+ refs: refTo('goal', task.name),
289
+ },
290
+ {
291
+ label: 'Adversary',
292
+ value: 'Unrestricted',
293
+ note: 'No fairness assumption forces an adversary choice (a hazard to appear, a missing edge to return)'
294
+ + (model.round !== 'fsync' ? ', apart from fair activation.' : '.'),
295
+ },
296
+ ],
297
+ },
298
+ {
299
+ title: 'Symmetry reduction',
300
+ facts: [{
301
+ label: 'Up to',
302
+ value: symmetry,
303
+ note: model.symmetries === 'none'
304
+ ? 'Disabled by the model (symmetries := none).'
305
+ : 'Configurations equal up to these automorphisms are merged. Derived from the equivariance declarations of the components, sensors and task.',
306
+ details: group.reasons,
307
+ }],
308
+ },
309
+ ];
310
+ const initChoices = choices(exp.agents.count, exp.agents.pos, ...model.components.flatMap(c => [
311
+ ...Object.values(c.init?.world ?? {}), ...Object.values(c.init?.agent ?? {}),
312
+ ]));
313
+ const acting = (h) => model.components.filter(c => c[h]).map(c => c.name);
314
+ /** A hook phase: what it does, or that no component acts in it. */
315
+ const hook = (h, what) => ({ phase: h, what: acting(h).length ? what : 'No component acts in this phase.', by: acting(h), choices: hookChoices(h) });
316
+ const hookChoices = (h) => choices(...model.components.map(c => c[h]));
317
+ const adversaryFlags = flags.filter(f => f.acts === 'adversary').map(f => f.name);
318
+ const round = [
319
+ {
320
+ phase: 'Initialisation',
321
+ what: 'Once: the agents are placed and the components set their initial values from the parameters.',
322
+ ...(model.components.some(c => c.init) ? { by: model.components.filter(c => c.init).map(c => c.name) } : {}),
323
+ choices: initChoices,
324
+ },
325
+ {
326
+ phase: 'Topology',
327
+ what: model.topology.kind === 'ring' ? 'Every edge is present.' : `The adversary removes up to ${model.topology.k} edge${model.topology.k === 1 ? '' : 's'} for this round.`,
328
+ choices: model.topology.kind === 'dynamicRing' ? ['missing edges'] : [],
329
+ },
330
+ hook('beforeLook', 'Components update their state, all reading the same configuration.'),
331
+ {
332
+ phase: 'Activation',
333
+ what: (model.round === 'fsync' ? 'Every agent able to act is activated.'
334
+ : model.round === 'sequential' ? 'Exactly one agent able to act is activated (none if no agent can act).'
335
+ : 'A nonempty set of the agents able to act is activated.')
336
+ + (model.frame.kind === 'dynamicReflection' ? " Each activated agent's orientation is chosen." : ''),
337
+ choices: [
338
+ ...(model.round !== 'fsync' ? ['activated agents (fair)'] : []),
339
+ ...(model.frame.kind === 'dynamicReflection' ? ['orientations'] : []),
340
+ ],
341
+ },
342
+ {
343
+ phase: 'Look & Compute',
344
+ what: `Each activated agent evaluates ${algorithm.uses.length ? algorithm.uses.join(', ') : 'its sensors'} once, then ${algorithm.name} decides its move${actions.length ? ` and actions (${actions.map(a => a.name).join(', ')})` : ''}.`
345
+ + (adversaryFlags.length ? ` The adversary decides for agents flagged ${adversaryFlags.join(', ')}.` : ''),
346
+ choices: [...choices(...sensors.map(x => x.s.body)), ...(adversaryFlags.length ? ['moves of agents flagged ' + adversaryFlags.join(', ')] : [])],
347
+ },
348
+ hook('beforeMove', 'Components read the decisions and update their state.'),
349
+ { phase: 'Move', what: 'Agents that decided and still act move, rigidly, along present edges.' },
350
+ hook('afterMove', 'Components update their state after the moves.'),
351
+ ];
352
+ // What each region of the Lean model translates, and where that is written.
353
+ const pcLines = lineTable(exp);
354
+ const origin = (o) => {
355
+ switch (o.kind) {
356
+ case 'composition': return { label: 'composition of the components', refs: modelRef('components') };
357
+ case 'model': return { label: `model ${model.name}`, refs: modelRef('topology') };
358
+ case 'component': return { label: `component ${o.name}`, refs: refTo('component', o.name) };
359
+ case 'sensor': return { label: `sensor ${o.name}`, refs: refTo('sensor', o.name) };
360
+ case 'definition': return { label: `definition ${o.name}`, refs: refTo('definition', o.name) };
361
+ case 'task': return { label: `task ${task.name}`, refs: refTo('task', task.name) };
362
+ case 'goal': return { label: `goal ${GOALS[task.goal].formula}`, refs: refTo('goal', task.name) };
363
+ case 'param': return { label: `param ${o.name}`, refs: expRef(expItem(new RegExp(`^\\s+param\\s+${o.name}\\b`))) };
364
+ case 'experiment': return o.name
365
+ ? { label: o.name === 'agents' ? 'number of agents' : 'initial configurations', refs: expRef(expItem(new RegExp(`^\\s+${o.name}\\b`))) }
366
+ : { label: 'statements to prove', refs: [...expRef(), ...refTo('goal', task.name)] };
367
+ case 'algorithm': {
368
+ if (o.pc === undefined)
369
+ return { label: o.name === 'line' ? 'line table (display only)' : `algorithm ${algorithm.name}`, refs: refTo('algorithm', algorithm.name) };
370
+ const line = pcLines[o.pc];
371
+ return { label: o.pc === 'start' ? 'start of main' : `resumes after line ${line}`, refs: refTo('algorithm', algorithm.name, line) };
372
+ }
373
+ }
374
+ };
375
+ const leanDescription = !lean ? undefined : 'error' in lean ? lean : {
376
+ module: lean.module,
377
+ text: lean.text,
378
+ blocks: lean.regions.map(r => ({ line: r.line, endLine: r.endLine, ...origin(r.origin) })),
379
+ library: lean.library.map(l => ({ module: l.module, text: l.text, refs: refTo('component', l.component) })),
380
+ };
381
+ const caveats = [
382
+ 'Checks are bounded: they cover the settings enumerated from the parameter domains, not every ring size.',
383
+ ...(exp.limits ? ['Exploration limits can leave a check incomplete.'] : []),
384
+ ...(algorithm.native ? [`The algorithm's step is native TypeScript (${algorithm.native}): it is not written in .sim and is not translated to Lean.`] : []),
385
+ ...(group.rotations ? ['Symmetry reduction relies on equivariance declarations, which are claims tested on bounded instances.'] : []),
386
+ ];
387
+ const expFile = exp.source?.file ?? algorithm.source?.file;
388
+ return {
389
+ experiment: exp.name,
390
+ about: expFile && sources[expFile] !== undefined ? about(sources[expFile]) : undefined,
391
+ summary: [
392
+ model.topology.kind === 'ring' ? 'ring' : `dynamic ring (${model.topology.k})`,
393
+ model.round.toUpperCase(),
394
+ model.memory === 'persistent' ? 'persistent memory' : 'oblivious',
395
+ model.frame.kind === 'common' ? 'common chirality' : model.frame.kind === 'dynamicReflection' ? 'dynamic orientation' : 'fixed orientation',
396
+ sensors.length ? `${sensors.length === 1 ? 'sensor' : 'sensors'}: ${sensors.map(x => x.s.name).join(', ')}` : 'no sensor',
397
+ ...model.components.map(componentName),
398
+ `${task.name}: ${GOALS[task.goal].formula}`,
399
+ ],
400
+ sections,
401
+ round,
402
+ sources: excerpts,
403
+ caveats,
404
+ ir: exp,
405
+ lean: leanDescription,
406
+ revision,
407
+ };
408
+ }
@@ -0,0 +1,42 @@
1
+ import { Snapshot } from "./snapshot.js";
2
+ export function label(witness) {
3
+ return witness.map(c => c.label).join('; ');
4
+ }
5
+ export function simExecution(system) {
6
+ const initial = system.initial();
7
+ if (!initial.length)
8
+ throw new Error('the experiment has no initial configuration');
9
+ const roots = initial.map(successor => ({ successor }));
10
+ const nodeAt = (path) => {
11
+ if (!(path[0] >= 0 && path[0] < roots.length))
12
+ throw new Error(`no initial configuration ${path[0]} (there are ${roots.length})`);
13
+ let node = roots[path[0]];
14
+ for (const i of path.slice(1)) {
15
+ const children = childrenOf(node);
16
+ if (i < 0 || i >= children.length)
17
+ throw new Error(`no child ${i} (there are ${children.length})`);
18
+ node = children[i];
19
+ }
20
+ return node;
21
+ };
22
+ const childrenOf = (node) => node.children ??= system.successors(node.successor.config).map(successor => ({ successor }));
23
+ return {
24
+ node: path => nodeAt(path).successor,
25
+ get: path => {
26
+ const node = nodeAt(path);
27
+ return node.snapshot ??= new Snapshot(node.successor.config, system);
28
+ },
29
+ getChildrenCount: path => childrenOf(nodeAt(path)).length,
30
+ getSiblingCount: path => path.length <= 1 ? roots.length : childrenOf(nodeAt(path.slice(0, -1))).length,
31
+ getRandomChildIndex: path => Math.floor(Math.random() * Math.max(1, childrenOf(nodeAt(path)).length)),
32
+ getEdgeLabel: path => label(nodeAt(path).successor.witness),
33
+ getEdgeWitness: path => nodeAt(path).successor.witness,
34
+ explain: path => {
35
+ const node = nodeAt(path);
36
+ if (path.length < 2)
37
+ return [];
38
+ // Replayed once per configuration: the explanation only depends on the step.
39
+ return node.explanations ??= system.explainAll(nodeAt(path.slice(0, -1)).successor.config, node.successor.key);
40
+ },
41
+ };
42
+ }