@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/LICENCE.txt +515 -0
- package/dist/check-proofs.js +76 -0
- package/dist/component.js +196 -0
- package/dist/differential.js +159 -0
- package/dist/library.js +30 -0
- package/dist/model.js +391 -0
- package/dist/names.js +3 -0
- package/dist/translate.js +398 -0
- package/package.json +54 -3
- package/src/check-proofs.ts +83 -0
- package/src/component.ts +195 -0
- package/src/differential.ts +159 -0
- package/src/library.ts +35 -0
- package/src/model.ts +387 -0
- package/src/names.ts +5 -0
- package/src/translate.ts +421 -0
- package/README.md +0 -3
package/src/component.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a component as a Lean module that is generic over the
|
|
3
|
+
* composition (plan, Section 5.1): the component sees its own world fields
|
|
4
|
+
* and agent fields through lenses (`Ctx`), its own flags through a class,
|
|
5
|
+
* and its own actions through a class. A model provides the instances. A
|
|
6
|
+
* lemma about the component then applies to every model that contains it.
|
|
7
|
+
*/
|
|
8
|
+
import type { Component, Expr, Field, Type } from '@bramas/simulant-lang/ir';
|
|
9
|
+
import { T, showType, typeEq } from '@bramas/simulant-lang/types';
|
|
10
|
+
import { fail, ident, Translator, type Env } from './translate';
|
|
11
|
+
|
|
12
|
+
export type ComponentInfo = {
|
|
13
|
+
name: string;
|
|
14
|
+
namespace: string;
|
|
15
|
+
/** Experiment parameters read by the initial values, with their types. */
|
|
16
|
+
inputs: Field[];
|
|
17
|
+
hasWorld: boolean;
|
|
18
|
+
hasFields: boolean;
|
|
19
|
+
enums: { name: string; ctors: string[] }[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Enum types mentioned by a type. */
|
|
23
|
+
export function enumsOf(t: Type, out: Map<string, string[]> = new Map()): Map<string, string[]> {
|
|
24
|
+
switch (t.k) {
|
|
25
|
+
case 'enum': out.set(t.name, t.ctors); break;
|
|
26
|
+
case 'option': case 'list': case 'multiset': enumsOf(t.of, out); break;
|
|
27
|
+
case 'record': for (const f of t.fields) enumsOf(f.type, out); break;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function enumsInExpr(e: Expr | undefined, out: Map<string, string[]>) {
|
|
33
|
+
if (!e) return;
|
|
34
|
+
const visit = (x: unknown): void => {
|
|
35
|
+
if (!x || typeof x !== 'object') return;
|
|
36
|
+
if (Array.isArray(x)) { x.forEach(visit); return; }
|
|
37
|
+
const o = x as Record<string, unknown>;
|
|
38
|
+
if (o.e === 'lit' || o.k) enumsOf((o.e === 'lit' ? o.type : o) as Type, out);
|
|
39
|
+
for (const v of Object.values(o)) visit(v);
|
|
40
|
+
};
|
|
41
|
+
visit(e);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function enumDecl(name: string, ctors: string[]): string {
|
|
45
|
+
return `inductive ${ident(name)} where\n${ctors.map(c => ` | ${ident(c)}`).join('\n')}\nderiving DecidableEq, Repr, Inhabited\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function structDecl(name: string, params: string, fields: Field[], type: (t: Type) => string): string {
|
|
49
|
+
if (!fields.length) return `structure ${name}${params} where\nderiving DecidableEq, Repr\n`;
|
|
50
|
+
return `structure ${name}${params} where\n${fields.map(f => ` ${ident(f.name)} : ${type(f.type)}`).join('\n')}\nderiving DecidableEq, Repr\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The experiment parameters read directly by initial values (`site := param site`). */
|
|
54
|
+
function inputsOf(c: Component): Field[] {
|
|
55
|
+
const inputs = new Map<string, Type>();
|
|
56
|
+
const fields = [...(c.world ?? []), ...(c.agent ?? [])];
|
|
57
|
+
const visit = (x: unknown, expected?: Type): void => {
|
|
58
|
+
if (!x || typeof x !== 'object') return;
|
|
59
|
+
const o = x as Record<string, unknown>;
|
|
60
|
+
if (o.e === 'param') {
|
|
61
|
+
if (!expected) fail(`${c.name}: the type of parameter ${o.name} is not known (use it directly as an initial value)`);
|
|
62
|
+
inputs.set(o.name as string, expected);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const v of Object.values(o)) if (v && typeof v === 'object') visit(v);
|
|
66
|
+
};
|
|
67
|
+
for (const f of fields) {
|
|
68
|
+
const init = c.init?.world?.[f.name] ?? c.init?.agent?.[f.name];
|
|
69
|
+
if (init?.e === 'param') inputs.set(init.name, f.type);
|
|
70
|
+
else visit(init);
|
|
71
|
+
}
|
|
72
|
+
return [...inputs].map(([name, type]) => ({ name, type })).sort((a, b) => a.name.localeCompare(b.name));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function componentInfo(c: Component, namespace: string): ComponentInfo {
|
|
76
|
+
const enums = new Map<string, string[]>();
|
|
77
|
+
for (const f of [...(c.params ?? []), ...(c.world ?? []), ...(c.agent ?? []), ...(c.actions ?? [])]) enumsOf(f.type, enums);
|
|
78
|
+
for (const e of [c.beforeLook, c.beforeMove, c.afterMove, ...Object.values(c.init?.world ?? {}), ...Object.values(c.init?.agent ?? {}),
|
|
79
|
+
...(c.sensors ?? []).map(s => s.body)]) enumsInExpr(e, enums);
|
|
80
|
+
return {
|
|
81
|
+
name: c.name, namespace, inputs: inputsOf(c),
|
|
82
|
+
hasWorld: !!c.world?.length, hasFields: !!c.agent?.length,
|
|
83
|
+
enums: [...enums].map(([name, ctors]) => ({ name, ctors })).filter(e => e.name !== 'Move'),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The Lean module of a component. */
|
|
88
|
+
export function generateComponent(c: Component, namespace: string): string {
|
|
89
|
+
const info = componentInfo(c, namespace);
|
|
90
|
+
const world = c.world ?? [];
|
|
91
|
+
const fields = c.agent ?? [];
|
|
92
|
+
const flags = c.flags ?? [];
|
|
93
|
+
const actions = c.actions ?? [];
|
|
94
|
+
const params = c.params ?? [];
|
|
95
|
+
const worldT = T.record(world);
|
|
96
|
+
const fieldsT = T.record(fields);
|
|
97
|
+
const enumNames = new Set(info.enums.map(e => e.name));
|
|
98
|
+
/** The part of the world the component owns. */
|
|
99
|
+
const own = world.length ? '(World σ.N)' : 'Unit';
|
|
100
|
+
|
|
101
|
+
const env = (node: string, N: string, k: string): Env => ({
|
|
102
|
+
node, agent: `Fin ${k}`, N, k, pub: 'σ.P',
|
|
103
|
+
typeName: t => {
|
|
104
|
+
if (t.k === 'enum' && enumNames.has(t.name)) return ident(t.name);
|
|
105
|
+
if (t.k === 'record' && typeEq(t, worldT)) return `World ${N}`;
|
|
106
|
+
if (t.k === 'record' && typeEq(t, fieldsT)) return `Fields ${N}`;
|
|
107
|
+
return fail(`${c.name}: no Lean name for ${showType(t)}`);
|
|
108
|
+
},
|
|
109
|
+
worldField: (C, f) => `(L.world.get ${C}.world).${ident(f)}`,
|
|
110
|
+
agentFlag: (a, f) => `(Flags.${ident(f)} ${a}.flags)`,
|
|
111
|
+
agentField: (a, f) => `(L.fields.get ${a}.fields).${ident(f)}`,
|
|
112
|
+
decisionAction: (d, a) => `(Actions.${ident(a)} ${d}.act)`,
|
|
113
|
+
param: name => ident(name),
|
|
114
|
+
paramType: name => info.inputs.find(i => i.name === name)?.type ?? fail(`unknown parameter ${name}`),
|
|
115
|
+
effect: (w, raise) => {
|
|
116
|
+
const worldCode = w.length ? `fun x => { x with ${w.map(([f, v]) => `${ident(f)} := ${v}`).join(', ')} }` : 'id';
|
|
117
|
+
const one = ([flag, r]: [string, { pred?: string; set?: string }]) => {
|
|
118
|
+
const cond = r.pred ? `${r.pred} a` : `(${r.set}).contains a`;
|
|
119
|
+
return `(if ${cond} then (Flags.raise_${ident(flag)} : σ.F) else FlagSet.none)`;
|
|
120
|
+
};
|
|
121
|
+
const raiseCode = raise.length
|
|
122
|
+
? raise.map(one).reduce((acc, x) => `FlagSet.merge ${acc} ${x}`)
|
|
123
|
+
: 'FlagSet.none';
|
|
124
|
+
return `({ world := ${worldCode}, raise := fun a => ${raiseCode} } : OwnEffect σ ${own})`;
|
|
125
|
+
},
|
|
126
|
+
ts: 'ts', ds: 'ds',
|
|
127
|
+
layout: { pub: T.pub, mem: T.unit, worldType: worldT, flags, agentFields: fields, actions },
|
|
128
|
+
defs: new Map((c.defs ?? []).map(d => [d.name, d])),
|
|
129
|
+
defName: n => ident(n),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const hookEnv = env('Fin σ.N', 'σ.N', 'σ.k');
|
|
133
|
+
const initEnv = env('Fin N', 'N', 'k');
|
|
134
|
+
const tr = new Translator(hookEnv);
|
|
135
|
+
const trInit = new Translator(initEnv);
|
|
136
|
+
const paramVars = params.map(p => [p.name, ident(p.name), p.type] as [string, string, Type]);
|
|
137
|
+
const paramBinders = params.map(p => `(${ident(p.name)} : ${tr.type(p.type)})`).join(' ');
|
|
138
|
+
const inputBinders = info.inputs.map(p => `(${ident(p.name)} : ${trInit.type(p.type)})`).join(' ');
|
|
139
|
+
const out: string[] = [];
|
|
140
|
+
|
|
141
|
+
out.push(`import Simulant\n\n/-! Generated from the IR of component \`${c.name}\` by packages/lean-gen. Do not edit. -/\n\nset_option linter.unusedVariables false\n`);
|
|
142
|
+
out.push(`namespace ${namespace}\nopen Simulant\n`);
|
|
143
|
+
for (const e of info.enums) out.push(enumDecl(e.name, e.ctors));
|
|
144
|
+
if (world.length) out.push(structDecl('World', ' (N : Nat)', world, t => trInit.type(t)));
|
|
145
|
+
if (fields.length) out.push(structDecl('Fields', ' (N : Nat)', fields, t => trInit.type(t)));
|
|
146
|
+
out.push(`/-- The flags declared by \`${c.name}\`, with the laws of a composition's flags. -/`);
|
|
147
|
+
if (flags.length) {
|
|
148
|
+
out.push(`class Flags (F : Type) [FlagSet F] where\n${flags.map(f => {
|
|
149
|
+
const n = ident(f.name);
|
|
150
|
+
return ` ${n} : F → Bool\n raise_${n} : F\n ${n}_raise : ${n} raise_${n} = true\n ${n}_merge : ∀ a b : F, ${n} (FlagSet.merge a b) = (${n} a || ${n} b)\n ${n}_none : ${n} (FlagSet.none : F) = false`;
|
|
151
|
+
}).join('\n')}\n`);
|
|
152
|
+
} else {
|
|
153
|
+
out.push('class Flags (F : Type) [FlagSet F] where\n');
|
|
154
|
+
}
|
|
155
|
+
out.push(actions.length
|
|
156
|
+
? `class Actions (A : Type) where\n${actions.map(a => ` ${ident(a.name)} : A → ${trInit.type(a.type)}`).join('\n')}\n`
|
|
157
|
+
: 'class Actions (A : Type) where\n');
|
|
158
|
+
out.push(`/-- What the composition gives the component: lenses on the state it owns. -/\nstructure Ctx (σ : Sig) where\n${
|
|
159
|
+
[world.length ? ' world : Lens σ.W (World σ.N)' : '', fields.length ? ' fields : Lens σ.X (Fields σ.N)' : ''].filter(Boolean).join('\n')}\n`);
|
|
160
|
+
|
|
161
|
+
// Initial values
|
|
162
|
+
if (world.length) {
|
|
163
|
+
const binds = world.map(f => {
|
|
164
|
+
const r = trInit.root(c.init!.world![f.name], paramVars);
|
|
165
|
+
return [f.name, r] as const;
|
|
166
|
+
});
|
|
167
|
+
out.push(`def initWorld {N : Nat} [NeZero N] ${paramBinders} ${inputBinders} : ND (World N) := do\n${
|
|
168
|
+
binds.map(([n, r]) => ` let ${ident(n)} ← ${trInit.nd(r)}`).join('\n')}\n pure { ${world.map(f => `${ident(f.name)} := ${ident(f.name)}`).join(', ')} }\n`);
|
|
169
|
+
}
|
|
170
|
+
for (const f of fields) {
|
|
171
|
+
const r = trInit.root(c.init!.agent![f.name], [['i', 'i', T.nat], ...paramVars]);
|
|
172
|
+
out.push(`def init_${ident(f.name)} {N : Nat} [NeZero N] ${paramBinders} ${inputBinders} (i : Nat) : ND (${trInit.type(f.type)}) :=\n ${trInit.nd(r)}\n`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
out.push(`section\nvariable {σ : Sig} [FlagSet σ.F] [Flags σ.F] [Actions σ.A]\n`);
|
|
176
|
+
for (const s of c.sensors ?? []) {
|
|
177
|
+
const r = tr.root(s.body, [['C', 'C', T.config], ['me', 'me', T.agent], ['frame', 'frame', T.frame], ...paramVars], 'C');
|
|
178
|
+
out.push(`def sensor_${ident(s.name)} (L : Ctx σ) ${paramBinders} (ts : TState) (C : Config σ) (me : Fin σ.k) (frame : Bool) : ND (${tr.type(r.t)}) :=\n ${tr.nd(r)}\n`);
|
|
179
|
+
}
|
|
180
|
+
const hook = (body: Expr | undefined, args: string) => {
|
|
181
|
+
if (!body) return `fun ${args} => pure ⟨id, fun _ => FlagSet.none⟩`;
|
|
182
|
+
return `fun ${args} => ${tr.nd(tr.root(body, [['C', 'C', T.config], ...paramVars], 'C'))}`;
|
|
183
|
+
};
|
|
184
|
+
out.push(`/-- The hooks, on the part of the world the component owns. -/
|
|
185
|
+
def hooks ${paramBinders} (L : Ctx σ) : Hooks σ ${own} where
|
|
186
|
+
beforeLook := ${hook(c.beforeLook, 'ts C')}
|
|
187
|
+
beforeMove := ${hook(c.beforeMove, 'ts C ds')}
|
|
188
|
+
afterMove := ${hook(c.afterMove, 'ts C')}
|
|
189
|
+
|
|
190
|
+
def component ${paramBinders} (L : Ctx σ) : Component σ :=
|
|
191
|
+
(hooks ${params.map(p => ident(p.name)).join(' ')} L).toComponent ${world.length ? 'L.world' : '(Lens.unit σ.W)'}
|
|
192
|
+
`);
|
|
193
|
+
out.push(`end\n\nend ${namespace}\n`);
|
|
194
|
+
return out.join('\n');
|
|
195
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Differential test of the generated Lean model against the interpreter
|
|
3
|
+
* (plan, Section 5.3): on a small instance, the computable Lean round and
|
|
4
|
+
* the interpreter must give the same successors, and every witness of the
|
|
5
|
+
* interpreter must be a witness in Lean (replay of interpreter paths).
|
|
6
|
+
*
|
|
7
|
+
* The interpreter explores the instance with agents kept in index order
|
|
8
|
+
* (as in Lean) and without symmetry reduction; a generated Lean file then
|
|
9
|
+
* evaluates the round on each configuration and prints the successors.
|
|
10
|
+
*/
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import type { Experiment, Type } from '@bramas/simulant-lang/ir';
|
|
15
|
+
import type { Layout } from '@bramas/simulant-lang/layout';
|
|
16
|
+
import { resolvePub } from '@bramas/simulant-lang/types';
|
|
17
|
+
import { Explorer } from '@bramas/simulant-engine/explorer';
|
|
18
|
+
import { System, type Config } from '@bramas/simulant-engine/system';
|
|
19
|
+
import { canonicalSort, type V } from '@bramas/simulant-engine/values';
|
|
20
|
+
import { generateModel } from './model';
|
|
21
|
+
import { fail } from './translate';
|
|
22
|
+
|
|
23
|
+
export type DiffReport = { states: number; successors: number; mismatches: string[] };
|
|
24
|
+
|
|
25
|
+
/** Groups the composed fields by owning component, in component order. */
|
|
26
|
+
function parts(lay: Layout, which: 'world' | 'agentFields') {
|
|
27
|
+
const owners: string[] = [];
|
|
28
|
+
for (const f of lay[which]) if (!owners.includes(f.owner)) owners.push(f.owner);
|
|
29
|
+
return owners.map(owner => ({ owner, fields: lay[which].filter(f => f.owner === owner) }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A Lean term for a value, with anonymous constructors (the expected type is always known). */
|
|
33
|
+
function term(v: V, t: Type): string {
|
|
34
|
+
switch (t.k) {
|
|
35
|
+
case 'unit': return '()';
|
|
36
|
+
case 'bool': case 'frame': case 'nat': return String(v);
|
|
37
|
+
case 'int': return `(${v} : Int)`;
|
|
38
|
+
case 'node': return `⟨${v}, by decide⟩`;
|
|
39
|
+
case 'move': return `Simulant.Move.${v}`;
|
|
40
|
+
case 'enum': return `.${v}`;
|
|
41
|
+
case 'option': return v === null ? 'none' : `(some ${term(v, t.of)})`;
|
|
42
|
+
case 'list': case 'multiset': return `[${(v as V[]).map(x => term(x, t.of)).join(', ')}]`;
|
|
43
|
+
case 'record': return `⟨${t.fields.map(f => term(v[f.name], f.type)).join(', ')}⟩`;
|
|
44
|
+
default: return fail(`no Lean term for values of type ${t.k}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let depth = 0;
|
|
49
|
+
/** Lean code printing `code` (of type `t`) as JSON. */
|
|
50
|
+
function json(code: string, t: Type): string {
|
|
51
|
+
switch (t.k) {
|
|
52
|
+
case 'unit': return '"null"';
|
|
53
|
+
case 'bool': case 'frame': case 'nat': case 'int': return `(toString ${code})`;
|
|
54
|
+
case 'node': return `(toString ${code}.val)`;
|
|
55
|
+
case 'move': case 'enum': return `("\\"" ++ reprStr ${code} ++ "\\"")`;
|
|
56
|
+
case 'option': {
|
|
57
|
+
const y = `y${depth++}`;
|
|
58
|
+
return `(match ${code} with | none => "null" | some ${y} => ${json(y, t.of)})`;
|
|
59
|
+
}
|
|
60
|
+
case 'list': case 'multiset': {
|
|
61
|
+
const y = `y${depth++}`;
|
|
62
|
+
return `("[" ++ ",".intercalate (${code}.map (fun ${y} => ${json(y, t.of)})) ++ "]")`;
|
|
63
|
+
}
|
|
64
|
+
case 'record':
|
|
65
|
+
return `("{" ++ ${t.fields.map(f => `"\\"${f.name}\\":" ++ ${json(`(${code}).${f.name}`, f.type)}`).join(' ++ "," ++ ')} ++ "}")`
|
|
66
|
+
.replace('"{" ++ ++ "}"', '"{}"');
|
|
67
|
+
default: return fail(`no JSON printer for type ${t.k}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Back to interpreter values: enum names lose their Lean namespace, multisets are sorted. */
|
|
72
|
+
function normalize(v: V, t: Type): V {
|
|
73
|
+
switch (t.k) {
|
|
74
|
+
case 'move': case 'enum': return String(v).split('.').pop();
|
|
75
|
+
case 'option': return v === null ? null : normalize(v, t.of);
|
|
76
|
+
case 'list': return v.map((x: V) => normalize(x, t.of));
|
|
77
|
+
case 'multiset': return canonicalSort(t.of)(v.map((x: V) => normalize(x, t.of)));
|
|
78
|
+
case 'record': return Object.fromEntries(t.fields.map(f => [f.name, normalize(v[f.name], f.type)]));
|
|
79
|
+
case 'unit': return null;
|
|
80
|
+
default: return v;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `mutate` edits the generated Lean source: tests use it to check that differences are detected. */
|
|
85
|
+
export function differential(exp: Experiment, params: Record<string, V>, leanDir: string, maxStates = 40,
|
|
86
|
+
mutate: (lean: string) => string = x => x): DiffReport {
|
|
87
|
+
const system = new System(exp, params, { sortAgents: false });
|
|
88
|
+
const explorer = new Explorer(system, { symmetry: false, maxStates });
|
|
89
|
+
explorer.run();
|
|
90
|
+
const configs = explorer.states.map(s => s.config);
|
|
91
|
+
const lay = system.lay;
|
|
92
|
+
const pub = lay.pub, mem = lay.mem;
|
|
93
|
+
const N = system.topo.size;
|
|
94
|
+
const k = configs[0]?.agents.length ?? 0;
|
|
95
|
+
const worldParts = parts(lay, 'world');
|
|
96
|
+
const fieldParts = parts(lay, 'agentFields');
|
|
97
|
+
const sig = `(sig ${N} ${k})`;
|
|
98
|
+
|
|
99
|
+
const agentTerm = (a: Config['agents'][number]) => `{ pos := ⟨${a.pos}, by decide⟩, flags := ⟨${lay.flags.map(f => String(!!a.f[f.name])).join(', ')}⟩, fields := ⟨${
|
|
100
|
+
fieldParts.map(p => `⟨${p.fields.map(f => term(a.f[f.name], resolvePub(f.type, pub))).join(', ')}⟩`).join(', ')}⟩, pub := ${term(a.pub, pub)}, mem := ${term(a.mem, mem)} }`;
|
|
101
|
+
const configTerm = (C: Config) => `({ agents := #v[${C.agents.map(agentTerm).join(', ')}], world := ⟨${
|
|
102
|
+
worldParts.map(p => `⟨${p.fields.map(f => term(C.world[f.name], f.type)).join(', ')}⟩`).join(', ')}⟩ } : Config ${sig})`;
|
|
103
|
+
|
|
104
|
+
depth = 0;
|
|
105
|
+
const agentJson = `"{\\"pos\\":" ++ toString a.pos.val ++ ",\\"pub\\":" ++ ${json('a.pub', pub)} ++ ",\\"mem\\":" ++ ${json('a.mem', mem)} ++ ",\\"f\\":{" ++ ${
|
|
106
|
+
[...lay.flags.map(f => `"\\"${f.name}\\":" ++ toString a.flags.${f.name}`),
|
|
107
|
+
...fieldParts.flatMap(p => p.fields.map(f => `"\\"${f.name}\\":" ++ ${json(`a.fields.${p.owner}.${f.name}`, f.type)}`))].join(' ++ "," ++ ') || '""'} ++ "}}"`;
|
|
108
|
+
const worldJson = `"{" ++ ${worldParts.flatMap(p => p.fields.map(f => `"\\"${f.name}\\":" ++ ${json(`C.world.${p.owner}.${f.name}`, f.type)}`)).join(' ++ "," ++ ') || '""'} ++ "}"`;
|
|
109
|
+
|
|
110
|
+
const ns = 'DiffModel';
|
|
111
|
+
const lean = `${generateModel(exp, ns)}
|
|
112
|
+
open Simulant ${ns}
|
|
113
|
+
|
|
114
|
+
def jAgent (a : Agent ${sig}) : String := ${agentJson}
|
|
115
|
+
def jConfig (C : Config ${sig}) : String :=
|
|
116
|
+
"{\\"agents\\":[" ++ ",".intercalate (C.agents.toList.map jAgent) ++ "],\\"world\\":" ++ (${worldJson}) ++ "}"
|
|
117
|
+
def jWitness (w : Witness) : String := "[" ++ ",".intercalate (w.map fun (s, i) => "[\\"" ++ s ++ "\\"," ++ toString i ++ "]") ++ "]"
|
|
118
|
+
|
|
119
|
+
def configs : List (Config ${sig}) := [
|
|
120
|
+
${configs.map(configTerm).join(',\n ')}]
|
|
121
|
+
|
|
122
|
+
#eval show IO Unit from do
|
|
123
|
+
for C in configs do
|
|
124
|
+
let succ := (model ${N} ${k}).roundA act0 C
|
|
125
|
+
IO.println ("[" ++ ",".intercalate (succ.map fun (w, _, C') => "[" ++ jWitness w ++ "," ++ jConfig C' ++ "]") ++ "]")
|
|
126
|
+
`;
|
|
127
|
+
const dir = join(leanDir, '.diff');
|
|
128
|
+
mkdirSync(dir, { recursive: true });
|
|
129
|
+
const file = join(dir, `${exp.name.replace(/\W+/g, '_')}.lean`);
|
|
130
|
+
writeFileSync(file, mutate(lean));
|
|
131
|
+
const out = execFileSync('lake', ['env', 'lean', file], { cwd: leanDir, encoding: 'utf8', maxBuffer: 1 << 28 });
|
|
132
|
+
const lines = out.split('\n').filter(l => l.startsWith('['));
|
|
133
|
+
if (lines.length !== configs.length) fail(`Lean printed ${lines.length} lines for ${configs.length} configurations:\n${out.slice(0, 2000)}`);
|
|
134
|
+
|
|
135
|
+
const normConfig = (c: { agents: V[]; world: V }): Config => ({
|
|
136
|
+
agents: c.agents.map((a: V) => ({
|
|
137
|
+
pos: a.pos, pub: normalize(a.pub, pub), mem: normalize(a.mem, mem),
|
|
138
|
+
f: Object.fromEntries([...lay.flags.map(f => [f.name, a.f[f.name]]),
|
|
139
|
+
...lay.agentFields.map(f => [f.name, normalize(a.f[f.name], f.type)])]),
|
|
140
|
+
})),
|
|
141
|
+
world: normalize(c.world, lay.worldType),
|
|
142
|
+
});
|
|
143
|
+
const mismatches: string[] = [];
|
|
144
|
+
let successors = 0;
|
|
145
|
+
configs.forEach((C, i) => {
|
|
146
|
+
const lean = (JSON.parse(lines[i]) as [[string, number][], { agents: V[]; world: V }][])
|
|
147
|
+
.map(([w, c]) => ({ w: JSON.stringify(w), key: system.key(normConfig(c)) }));
|
|
148
|
+
const engine = system.successors(C).map(s => ({ w: JSON.stringify(s.witness.map(c => [c.site, c.index])), key: s.key }));
|
|
149
|
+
successors += engine.length;
|
|
150
|
+
const leanKeys = new Set(lean.map(x => x.key));
|
|
151
|
+
const engineKeys = new Set(engine.map(x => x.key));
|
|
152
|
+
for (const key of engineKeys) if (!leanKeys.has(key)) mismatches.push(`state ${i}: successor ${key} only in the interpreter`);
|
|
153
|
+
for (const key of leanKeys) if (!engineKeys.has(key)) mismatches.push(`state ${i}: successor ${key} only in Lean`);
|
|
154
|
+
for (const e of engine) {
|
|
155
|
+
if (!lean.some(l => l.key === e.key && l.w === e.w)) mismatches.push(`state ${i}: witness ${e.w} of ${e.key} is not a Lean witness`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return { states: configs.length, successors, mismatches };
|
|
159
|
+
}
|
package/src/library.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the component library (`lean/Simulant/Library/*.lean`) from the
|
|
3
|
+
* standard library's IR: npx tsx packages/lean-gen/src/library.ts
|
|
4
|
+
*/
|
|
5
|
+
import { writeFileSync } from 'node:fs';
|
|
6
|
+
import { stdlibElaborator } from '@bramas/simulant-lang/elab/node';
|
|
7
|
+
import type { Component } from '@bramas/simulant-lang/ir';
|
|
8
|
+
import { resolveComponent } from '@bramas/simulant-lang/standalone';
|
|
9
|
+
import { generateComponent } from './component';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The library's components, from the `.sim` standard library. Their
|
|
13
|
+
* arguments do not matter: parameters stay symbolic.
|
|
14
|
+
*/
|
|
15
|
+
export const libraryComponents: Component[] = (() => {
|
|
16
|
+
const elab = stdlibElaborator();
|
|
17
|
+
return ['Hazards.BlackHole .mayEmerge', 'Faults.CrashStop 1', 'Faults.CrashAtNode', 'Storage.Pebbles',
|
|
18
|
+
'Frames.PersistentReflection', 'Monitors.Visited'].map(e => resolveComponent(elab.evaluate(e) as Component));
|
|
19
|
+
})();
|
|
20
|
+
|
|
21
|
+
export const libraryNamespace = (name: string) => `Simulant.Library.${name}`;
|
|
22
|
+
|
|
23
|
+
export function generateLibrary(dir: URL) {
|
|
24
|
+
const names: string[] = [];
|
|
25
|
+
for (const c of libraryComponents) {
|
|
26
|
+
writeFileSync(new URL(`Library/${c.name}.lean`, dir), generateComponent(c, libraryNamespace(c.name)));
|
|
27
|
+
names.push(c.name);
|
|
28
|
+
}
|
|
29
|
+
writeFileSync(new URL('Library.lean', dir), names.map(n => `import Simulant.Library.${n}\n`).join(''));
|
|
30
|
+
return names;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
34
|
+
console.log(generateLibrary(new URL('../../../lean/Simulant/', import.meta.url)).join('\n'));
|
|
35
|
+
}
|