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