@bramas/simulant-lean-gen 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/model.js ADDED
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Generates the Lean model file of an experiment (plan, Section 5.1): the
3
+ * composed types, the instances that give each component its part of the
4
+ * state, the model, the algorithm, the task, the initial configurations,
5
+ * the parameter domains, and the statements to prove.
6
+ */
7
+ import { check } from "@bramas/simulant-lang/check";
8
+ import { showType, T, typeEq } from "@bramas/simulant-lang/types";
9
+ import { componentInfo, enumDecl, enumsInExpr, enumsOf, generateComponent, structDecl } from "./component.js";
10
+ import { libraryComponents, libraryNamespace } from "./library.js";
11
+ import { fail, ident, Translator, wrap } from "./translate.js";
12
+ /** Whether a component of the experiment is the library's (same IR, whatever the arguments). */
13
+ export function isLibrary(c) {
14
+ const lib = libraryComponents.find(l => l.name === c.name);
15
+ const strip = (x) => JSON.stringify({ ...x, args: undefined });
16
+ return !!lib && strip(lib) === strip(c);
17
+ }
18
+ export function generateModel(exp, namespace) {
19
+ return generateMappedModel(exp, namespace).text;
20
+ }
21
+ /**
22
+ * A root `(match x with | a => ... | b => ...)` with one case per line. The term is the same:
23
+ * Lean reads line breaks inside parentheses as spaces.
24
+ */
25
+ export function casesOnLines(code, indent) {
26
+ if (!code.startsWith('(match ') || !code.endsWith(')'))
27
+ return code;
28
+ const cuts = [];
29
+ let depth = 0;
30
+ for (let i = 0; i < code.length; i++) {
31
+ const ch = code[i];
32
+ if (ch === '"') {
33
+ for (i++; i < code.length && code[i] !== '"'; i++)
34
+ if (code[i] === '\\')
35
+ i++;
36
+ }
37
+ else if ('([{'.includes(ch))
38
+ depth++;
39
+ else if (')]}'.includes(ch)) {
40
+ if (--depth === 0 && i !== code.length - 1)
41
+ return code;
42
+ }
43
+ else if (depth === 1 && ch === '|' && code[i - 1] === ' ' && code[i + 1] === ' ')
44
+ cuts.push(i);
45
+ }
46
+ if (!cuts.length)
47
+ return code;
48
+ return code.slice(0, cuts[0] - 1) + cuts.map((c, j) => `\n${indent}${code.slice(c, j + 1 < cuts.length ? cuts[j + 1] - 1 : undefined)}`).join('');
49
+ }
50
+ export function generateMappedModel(exp, namespace) {
51
+ const checked = check(exp);
52
+ if (checked.errors.length)
53
+ fail(`the experiment does not typecheck:\n${checked.errors.join('\n')}`);
54
+ const lay = checked.layout;
55
+ const { model, algorithm, task } = exp;
56
+ if (model.topology.size.e !== 'param')
57
+ fail('the topology size must be a parameter');
58
+ const sizeParam = model.topology.size.name;
59
+ const params = exp.params;
60
+ if (params[0]?.name !== sizeParam)
61
+ fail(`the ring size ${sizeParam} must be the first parameter`);
62
+ // Components: library ones are referenced, others are generated in this file.
63
+ const comps = model.components.map(c => {
64
+ const library = isLibrary(c);
65
+ const ns = library ? libraryNamespace(c.name) : `${namespace}.Components.${c.name}`;
66
+ return { c, ns, library, info: componentInfo(c, ns) };
67
+ });
68
+ const ownerNs = new Map(comps.map(x => [x.c.name, x.ns]));
69
+ // Enums: those of components are qualified by their namespace.
70
+ const enumNames = new Map();
71
+ for (const { info, ns } of comps)
72
+ for (const e of info.enums)
73
+ enumNames.set(e.name, `${ns}.${ident(e.name)}`);
74
+ const local = new Map();
75
+ const allDefs = [...(model.defs ?? []), ...(algorithm.defs ?? []), ...(task.defs ?? [])];
76
+ for (const t of [algorithm.pub, algorithm.mem, ...params.map(p => p.type), ...allDefs.flatMap(d => [d.ret, ...d.params.map(p => p.type)])])
77
+ enumsOf(t, local);
78
+ for (const e of [algorithm.step, algorithm.init.pub, algorithm.init.mem, algorithm.line, task.verdict,
79
+ ...model.sensors.map(s => s.body), ...params.map(p => p.domain), exp.agents.pos, exp.agents.count,
80
+ ...allDefs.map(d => d.body)])
81
+ enumsInExpr(e, local);
82
+ for (const name of [...local.keys()]) {
83
+ if (enumNames.has(name) || name === 'Move')
84
+ local.delete(name);
85
+ else
86
+ enumNames.set(name, ident(name));
87
+ }
88
+ // Record types: the algorithm's public state, memory and view, then anonymous ones.
89
+ const records = [];
90
+ let anonymous = 0;
91
+ const out = [];
92
+ const env = (node, k) => ({
93
+ node, agent: `Fin ${k}`, N: 'N', k, pub: 'Pub',
94
+ typeName: t => {
95
+ if (t.k === 'enum')
96
+ return enumNames.get(t.name) ?? fail(`unknown enum ${t.name}`);
97
+ if (t.k !== 'record')
98
+ return fail(`no name for ${showType(t)}`);
99
+ const known = records.find(r => typeEq(r.t, t));
100
+ if (known)
101
+ return known.name;
102
+ // The types of the fields are named (and declared) first.
103
+ const name = `Rec${anonymous++}`;
104
+ const decl = structDecl(name, '', t.fields, x => modelTr.type(x));
105
+ records.push({ t, name, decl });
106
+ return name;
107
+ },
108
+ worldField: (C, f) => `${C}.world.${ident(lay.world.find(w => w.name === f).owner)}.${ident(f)}`,
109
+ agentFlag: (a, f) => `${a}.flags.${ident(f)}`,
110
+ agentField: (a, f) => `${a}.fields.${ident(lay.agentFields.find(x => x.name === f).owner)}.${ident(f)}`,
111
+ decisionAction: () => fail('decisions are only read by components'),
112
+ param: name => ident(name),
113
+ paramType: name => params.find(p => p.name === name)?.type ?? fail(`unknown parameter ${name}`),
114
+ layout: lay,
115
+ defs: new Map([...(model.defs ?? []), ...(algorithm.defs ?? []), ...(task.defs ?? [])].map(d => [d.name, d])),
116
+ defName: n => ident(n),
117
+ });
118
+ const modelTr = new Translator(env('Fin N', 'k'));
119
+ for (const [name, t] of [['Pub', algorithm.pub], ['Mem', algorithm.mem], ['View', lay.view]]) {
120
+ if (t.k === 'record') {
121
+ const decl = structDecl(name, '', t.fields, x => modelTr.type(x));
122
+ records.push({ t, name, decl });
123
+ }
124
+ }
125
+ const typeOf = (t) => t.k === 'record' ? records.find(r => typeEq(r.t, t)).name : modelTr.type(t);
126
+ const pubT = typeOf(algorithm.pub);
127
+ const memT = typeOf(algorithm.mem);
128
+ const body = [];
129
+ const emit = (text, origin = { kind: 'composition' }) => body.push({ text, origin });
130
+ // Flags
131
+ const flags = lay.flags;
132
+ emit(structDecl('Flags', '', flags.map(f => ({ name: f.name, type: T.bool })), () => 'Bool'));
133
+ const anyOf = (names, f) => names.length ? names.map(n => `${f}.${ident(n)}`).join(' || ') : 'false';
134
+ const never = flags.filter(f => f.acts === 'never').map(f => f.name);
135
+ const adversary = flags.filter(f => f.acts === 'adversary').map(f => f.name);
136
+ emit(`instance : FlagSet Flags where
137
+ none := { ${flags.map(f => `${ident(f.name)} := false`).join(', ')} }
138
+ merge f g := { ${flags.map(f => `${ident(f.name)} := f.${ident(f.name)} || g.${ident(f.name)}`).join(', ')} }
139
+ correct f := !(${anyOf(flags.map(f => f.name), 'f')})
140
+ present f := !(${anyOf(flags.filter(f => f.visibility === 'hidden').map(f => f.name), 'f')})
141
+ acts f := if ${anyOf(never, 'f')} then .never else if ${anyOf(adversary, 'f')} then .adversary else .algorithm
142
+ `);
143
+ const fv = (p) => flags.map((_, i) => `${p}${i}`);
144
+ emit(`instance : LawfulFlagSet Flags where
145
+ acts_merge_never f g := by
146
+ obtain ⟨${fv('a').join(', ')}⟩ := f
147
+ obtain ⟨${fv('b').join(', ')}⟩ := g
148
+ ${[...fv('a'), ...fv('b')].map(v => `cases ${v} <;> `).join('')}decide
149
+ `);
150
+ for (const { c, ns } of comps) {
151
+ const own = (c.flags ?? []);
152
+ if (!own.length) {
153
+ emit(`instance : ${ns}.Flags Flags where\n`);
154
+ continue;
155
+ }
156
+ emit(`instance : ${ns}.Flags Flags where
157
+ ${own.map(f => {
158
+ const n = ident(f.name);
159
+ return ` ${n} f := f.${n}
160
+ raise_${n} := { (FlagSet.none : Flags) with ${n} := true }
161
+ ${n}_raise := rfl
162
+ ${n}_merge _ _ := rfl
163
+ ${n}_none := rfl`;
164
+ }).join('\n')}
165
+ `);
166
+ }
167
+ // Actions
168
+ const actionFields = lay.actions.map(a => ({ name: a.name, type: a.type }));
169
+ emit(structDecl('Actions', '', actionFields, t => modelTr.type(t)));
170
+ emit(`def act0 : Actions := { ${lay.actions.map(a => `${ident(a.name)} := ${modelTr.lit(a.default, a.type)}`).join(', ')} }\n`);
171
+ for (const { c, ns } of comps) {
172
+ const own = c.actions ?? [];
173
+ emit(own.length
174
+ ? `instance : ${ns}.Actions Actions where\n${own.map(a => ` ${ident(a.name)} a := a.${ident(a.name)}`).join('\n')}\n`
175
+ : `instance : ${ns}.Actions Actions where\n`);
176
+ }
177
+ // World and agent fields: one part per component that has some.
178
+ const withWorld = comps.filter(x => x.info.hasWorld);
179
+ const withFields = comps.filter(x => x.info.hasFields);
180
+ emit(`structure World (N : Nat) where\n${withWorld.map(x => ` ${ident(x.c.name)} : ${x.ns}.World N`).join('\n')}\nderiving DecidableEq, Repr\n`);
181
+ emit(`structure Fields (N : Nat) where\n${withFields.map(x => ` ${ident(x.c.name)} : ${x.ns}.Fields N`).join('\n')}\nderiving DecidableEq, Repr\n`);
182
+ const viewT = lay.view.k === 'record' && lay.view.fields.length ? 'View' : 'Unit';
183
+ emit(`abbrev sig (N k : Nat) : Sig :=
184
+ { N := N, k := k, F := Flags, X := Fields N, P := ${pubT}, M := ${memT}, A := Actions, W := World N, V := ${viewT} }
185
+ `);
186
+ for (const { c, ns, info } of comps) {
187
+ const parts = [
188
+ info.hasWorld ? ` world := ⟨fun w => w.${ident(c.name)}, fun w x => { w with ${ident(c.name)} := x }⟩` : '',
189
+ info.hasFields ? ` fields := ⟨fun x => x.${ident(c.name)}, fun x y => { x with ${ident(c.name)} := y }⟩` : '',
190
+ ].filter(Boolean);
191
+ emit(`def ctx_${c.name} (N k : Nat) : ${ns}.Ctx (sig N k) where\n${parts.join('\n')}\n`);
192
+ }
193
+ // Definitions on values (callees first), then sensors, view, algorithm, model, task
194
+ const defTr = new Translator(env('Fin N', 'k'));
195
+ for (const d of defTr.env.defs.values()) {
196
+ const sig = [...d.params.map(p => p.type), d.ret];
197
+ if (sig.some(mentionsRing))
198
+ fail(`definition ${d.name}: definitions on nodes, agents or configurations have no Lean translation yet`);
199
+ const r = defTr.root(d.body, d.params.map(p => [p.name, ident(p.name), p.type]));
200
+ if (r.nd)
201
+ fail(`definition ${d.name} contains choices: it has no Lean translation yet`);
202
+ const binders = d.params.map(p => `(${ident(p.name)} : ${defTr.type(defTr.resolve(p.type))})`).join(' ');
203
+ emit(`def ${ident(d.name)}${binders ? ' ' + binders : ''} : ${defTr.type(defTr.resolve(d.ret))} :=\n ${r.code}\n`, { kind: 'definition', name: d.name });
204
+ }
205
+ emit('section\nvariable (N k : Nat)\n', { kind: 'model' });
206
+ const sensorVars = [['C', 'C', T.config], ['me', 'me', T.agent], ['frame', 'frame', T.frame]];
207
+ const viewBinds = [];
208
+ for (const name of algorithm.uses) {
209
+ const s = lay.sensors.find(s => s.name === name);
210
+ const owner = comps.find(x => x.c.name === s.owner);
211
+ if (owner) {
212
+ const args = (owner.c.params ?? []).map(p => wrap(modelTr.lit(owner.c.args[p.name], p.type)));
213
+ viewBinds.push(`let ${ident(name)} ← ${owner.ns}.sensor_${ident(name)} (ctx_${owner.c.name} N k) ${args.join(' ')} ts C me frame`);
214
+ }
215
+ else {
216
+ const r = modelTr.root(s.body, sensorVars, 'C');
217
+ emit(`def sensor_${ident(name)} (ts : TState) (C : Config (sig N k)) (me : Fin k) (frame : Bool) : ND (${modelTr.type(r.t)}) :=\n ${modelTr.nd(r)}\n`, { kind: 'sensor', name });
218
+ viewBinds.push(`let ${ident(name)} ← sensor_${ident(name)} N k ts C me frame`);
219
+ }
220
+ }
221
+ emit(`def view (ts : TState) (C : Config (sig N k)) (me : Fin k) (frame : Bool) : ND ${viewT} := do
222
+ ${viewBinds.map(b => ` ${b}\n`).join('')} pure ${viewT === 'View' ? `{ ${algorithm.uses.map(n => `${ident(n)} := ${ident(n)}`).join(', ')} }` : '()'}
223
+ `, { kind: 'algorithm' });
224
+ const algTr = new Translator(env('Fin N', 'k'));
225
+ const stepVars = [['mem', 'mem', algorithm.mem], ['pub', 'pub', algorithm.pub], ['view', 'view', lay.view]];
226
+ const actOf = (get) => `{ ${lay.actions.map(a => `${ident(a.name)} := ${get(a.name) ?? modelTr.lit(a.default, a.type)}`).join(', ')} }`;
227
+ let stepCode;
228
+ if (algorithm.native)
229
+ fail(`algorithm ${algorithm.name} is native: it has no Lean translation`);
230
+ if (algorithm.step.e === 'record') {
231
+ const f = algorithm.step.fields;
232
+ const tr = (n) => algTr.root(f[n], stepVars).code;
233
+ stepCode = `{ move := ${tr('move')}, pub := ${tr('pub')}, mem := ${tr('mem')}, act := ${actOf(n => f[n] ? tr(n) : undefined)} }`;
234
+ }
235
+ else {
236
+ const r = algTr.root(algorithm.step, stepVars);
237
+ const has = (n) => r.t.k === 'record' && r.t.fields.some(x => x.name === n);
238
+ stepCode = `let d := ${casesOnLines(r.code, ' ')}\n { move := d.move, pub := d.pub, mem := d.mem, act := ${actOf(n => has(n) ? `d.${ident(n)}` : undefined)} }`;
239
+ }
240
+ emit(`def step (mem : ${memT}) (pub : ${pubT}) (view : ${viewT}) : Out (sig N k) :=\n ${stepCode}\n`, { kind: 'algorithm', name: 'step' });
241
+ const topo = model.topology.kind === 'ring' ? 'Ring.staticStates' : `Ring.dynamicStates N ${model.topology.k}`;
242
+ const frame = model.frame.kind === 'common' ? '.common' : model.frame.kind === 'dynamicReflection' ? '.dynamic'
243
+ : `.persistent (fun x => ${(() => {
244
+ const f = model.frame.field;
245
+ return `x.${ident(lay.agentFields.find(a => a.name === f).owner)}.${ident(f)}`;
246
+ })()})`;
247
+ const compCode = comps.map(({ c, ns }) => {
248
+ const args = (c.params ?? []).map(p => wrap(modelTr.lit(c.args[p.name], p.type)));
249
+ return `${ns}.component ${args.join(' ')} (ctx_${c.name} N k)`;
250
+ });
251
+ emit(`def model : Model (sig N k) where
252
+ tstates := ${topo}
253
+ components := [${compCode.join(',\n ')}]
254
+ frame := ${frame}
255
+ round := .${model.round}
256
+ view := view N k
257
+ step := step N k
258
+ `, { kind: 'model' });
259
+ const taskTr = new Translator(env('Fin N', 'k'));
260
+ emit(`def verdict (C : Config (sig N k)) : Verdict :=\n ${taskTr.root(task.verdict, [['C', 'C', T.config]], 'C').code}\n`, { kind: 'task' });
261
+ if (algorithm.line)
262
+ emit(`def line (mem : ${memT}) : Nat :=\n ${casesOnLines(algTr.root(algorithm.line, [['mem', 'mem', algorithm.mem]]).code, ' ')}\n`, { kind: 'algorithm', name: 'line' });
263
+ emit('end\n', { kind: 'model' });
264
+ emit(`def goal : Goal := .${task.goal}\n`, { kind: 'goal' });
265
+ // Each component's part of the world is written by that component only.
266
+ for (const { c, ns } of withWorld) {
267
+ emit(`instance (N k : Nat) : LawfulLens (ctx_${c.name} N k).world := ⟨fun _ _ => rfl⟩\n`);
268
+ const cases = comps.map((x, i) => x.c.name === c.name
269
+ ? ' · exact Or.inl rfl'
270
+ : ' · exact Or.inr (Hooks.toComponent_preserves _ _ _ fun _ _ => rfl)');
271
+ emit(`/-- The other components of the model preserve the part of the world that \`${c.name}\` owns. -/
272
+ theorem others_preserve_${c.name} (N k : Nat) :
273
+ ∀ c ∈ (model N k).components, c = ${compCode[comps.findIndex(x => x.c.name === c.name)]} ∨ c.Preserves (ctx_${c.name} N k).world.get := by
274
+ intro c hc
275
+ simp only [model, List.mem_cons, List.not_mem_nil, or_false] at hc
276
+ rcases hc with ${comps.map(() => 'rfl').join(' | ')}
277
+ ${cases.join('\n')}
278
+ `);
279
+ }
280
+ // Parameters, initial configurations, statements
281
+ const initTr = new Translator(env('Fin N', 'k'));
282
+ const binder = (p) => p.name === sizeParam ? `(N : Nat) [NeZero N]` : `(${ident(p.name)} : ${initTr.type(p.type)})`;
283
+ const kCode = initTr.root(exp.agents.count, []).code;
284
+ const args = params.map(p => p.name === sizeParam ? 'N' : ident(p.name)).join(' ');
285
+ for (const [i, p] of params.entries()) {
286
+ if (!p.domain)
287
+ continue;
288
+ const r = initTr.root(p.domain, []);
289
+ const before = params.slice(0, i).map(binder).join(' ');
290
+ emit(`def domain_${ident(p.name)} ${before} : List (${initTr.type(p.type)}) :=\n ${r.code}\n`, { kind: 'param', name: p.name });
291
+ }
292
+ const i = [['i', 'i', T.nat]];
293
+ const pos = initTr.root(exp.agents.pos, i).code;
294
+ const pub = initTr.root(algorithm.init.pub, i).code;
295
+ const mem = initTr.root(algorithm.init.mem, i).code;
296
+ const initLines = [];
297
+ const fieldInits = [];
298
+ for (const { c, ns, info } of withFields) {
299
+ const cargs = (c.params ?? []).map(p => wrap(initTr.lit(c.args[p.name], p.type)));
300
+ const inputs = info.inputs.map(x => ident(x.name));
301
+ for (const f of c.agent) {
302
+ const v = `f_${c.name}_${f.name}`;
303
+ initLines.push(`let ${v} ← (List.range ${wrap(kCode)}).mapM (fun i => ${ns}.init_${ident(f.name)} (N := N) ${[...cargs, ...inputs].join(' ')} i)`);
304
+ fieldInits.push(`${ident(c.name)} := { ${ident(f.name)} := ${v}[i]! }`);
305
+ }
306
+ }
307
+ for (const { c, ns, info } of withWorld) {
308
+ const cargs = (c.params ?? []).map(p => wrap(initTr.lit(c.args[p.name], p.type)));
309
+ initLines.push(`let w_${c.name} ← ${ns}.initWorld (N := N) ${[...cargs, ...info.inputs.map(x => ident(x.name))].join(' ')}`);
310
+ }
311
+ emit(`def count ${params.map(binder).join(' ')} : Nat := ${kCode}\n`, { kind: 'experiment', name: 'agents' });
312
+ emit(`def init ${params.map(binder).join(' ')} : ND (Config (sig N (count ${args}))) := do
313
+ ${initLines.map(l => ` ${l}\n`).join('')} pure {
314
+ agents := Vector.ofFn fun j =>
315
+ let i := j.val
316
+ { pos := ${pos}, flags := FlagSet.none, fields := { ${fieldInits.join(', ')} }, pub := ${pub}, mem := ${mem} },
317
+ world := { ${withWorld.map(x => `${ident(x.c.name)} := w_${x.c.name}`).join(', ')} } }
318
+ `, { kind: 'experiment', name: 'position' });
319
+ const quant = params.map(p => p.domain
320
+ ? `∀ ${binder(p)}, ${ident(p.name === sizeParam ? 'N' : p.name)} ∈ domain_${ident(p.name)} ${params.slice(0, params.indexOf(p)).map(q => q.name === sizeParam ? 'N' : ident(q.name)).join(' ')} →`
321
+ : `∀ ${binder(p)},`).join('\n ');
322
+ const m = `(model N (count ${args}))`;
323
+ emit(`/-- Every admissible execution, for every value of the parameters, satisfies the task. -/
324
+ def Statement : Prop :=
325
+ ${quant}
326
+ ${m}.Correct act0 (init ${args}) (verdict N (count ${args})) goal
327
+
328
+ /-- Non-vacuity: an initial configuration exists, every reachable configuration has a successor,
329
+ and an admissible execution exists. -/
330
+ def NonVacuity : Prop :=
331
+ ${quant}
332
+ ${m}.HasInitial (init ${args}) ∧ ${m}.Total act0 (init ${args}) ∧ ${m}.HasExecution act0 (init ${args})
333
+ `, { kind: 'experiment' });
334
+ // Assemble: header, local enums and components, records, body.
335
+ const push = (text, origin) => out.push({ text, origin });
336
+ push(`import Simulant\n${[...new Set(comps.filter(x => x.library).map(x => `import ${x.ns}`))].join('\n')}\n`);
337
+ push(`/-! Generated from the IR of experiment \`${exp.name}\` by packages/lean-gen. Do not edit:
338
+ regenerate it. Proofs go in a separate file. -/\n\nset_option linter.unusedVariables false\n`);
339
+ for (const { c, ns, library } of comps) {
340
+ if (!library)
341
+ push(generateComponent(c, ns).replace(/^import Simulant\n/, ''), { kind: 'component', name: c.name });
342
+ }
343
+ push(`namespace ${namespace}\nopen Simulant\n`);
344
+ // The program counter's type comes from the algorithm; other enums from `inductive` declarations.
345
+ const pcType = algorithm.mem.k === 'record' ? algorithm.mem.fields.find(f => f.name === 'pc')?.type : undefined;
346
+ for (const [name, ctors] of local)
347
+ push(enumDecl(name, ctors), pcType?.k === 'enum' && pcType.name === name ? { kind: 'algorithm' } : undefined);
348
+ // Records are declared after the types they mention.
349
+ const recordOrigin = { Pub: { kind: 'algorithm' }, Mem: { kind: 'algorithm' }, View: { kind: 'algorithm' } };
350
+ for (const r of records)
351
+ push(r.decl, recordOrigin[r.name]);
352
+ out.push(...body);
353
+ push(`end ${namespace}\n`);
354
+ // Lines of each chunk (joined by newlines), and of each case of the step.
355
+ const regions = [];
356
+ let line = 1;
357
+ for (const chunk of out) {
358
+ const lines = chunk.text.split('\n');
359
+ while (lines.length > 1 && lines[lines.length - 1] === '')
360
+ lines.pop();
361
+ if (chunk.origin) {
362
+ regions.push({ line, endLine: line + lines.length - 1, origin: chunk.origin });
363
+ if (chunk.origin.name === 'step' && pcType?.k === 'enum') {
364
+ const cases = lines.flatMap((l, i) => {
365
+ const m = new RegExp(`^\\s*\\| ${ident(pcType.name)}\\.(\\w+) =>`).exec(l);
366
+ return m ? [{ i, pc: m[1] }] : [];
367
+ });
368
+ cases.forEach((c, j) => regions.push({
369
+ line: line + c.i, endLine: line + (j + 1 < cases.length ? cases[j + 1].i - 1 : c.i),
370
+ origin: { kind: 'algorithm', name: 'step', pc: c.pc },
371
+ }));
372
+ }
373
+ }
374
+ line += chunk.text.split('\n').length;
375
+ }
376
+ return { text: out.map(c => c.text).join('\n'), regions };
377
+ }
378
+ /** Whether a type needs the ring size or the agents in Lean. */
379
+ function mentionsRing(t) {
380
+ switch (t.k) {
381
+ case 'node':
382
+ case 'agent':
383
+ case 'config':
384
+ case 'pub': return true;
385
+ case 'option':
386
+ case 'list':
387
+ case 'multiset': return mentionsRing(t.of);
388
+ case 'record': return t.fields.some(f => mentionsRing(f.type));
389
+ default: return false;
390
+ }
391
+ }
package/dist/names.js ADDED
@@ -0,0 +1,3 @@
1
+ /** Lean names of generated files, shared by the CLI and the page (no Node imports). */
2
+ /** `black-hole-search-algo1-new` → `BlackHoleSearchAlgo1New`. */
3
+ export const leanModuleName = (simulation) => simulation.split(/[^A-Za-z0-9]+/).filter(Boolean).map(w => w[0].toUpperCase() + w.slice(1)).join('');