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