@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.
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Translation of IR expressions to Lean terms (plan, Section 5.1).
3
+ *
4
+ * A deterministic expression becomes a plain term; an expression with a
5
+ * choice becomes a term of type `ND T`, built with `do` and `←`, in the
6
+ * interpreter's evaluation order, so that witnesses coincide. Choice labels
7
+ * are only for display and are dropped.
8
+ */
9
+ import { BUILTINS } from "@bramas/simulant-lang/builtins";
10
+ import { fieldType, isCollection, MOVES, resolvePub, showType, T, typeEq, VERDICTS } from "@bramas/simulant-lang/types";
11
+ export class GenError extends Error {
12
+ }
13
+ const KEYWORDS = new Set([
14
+ 'at', 'by', 'do', 'else', 'end', 'fun', 'from', 'have', 'if', 'in', 'let', 'match', 'open', 'show', 'then', 'with',
15
+ 'where', 'structure', 'class', 'instance', 'def', 'theorem', 'namespace', 'section', 'variable', 'import', 'mut',
16
+ 'return', 'for', 'unless', 'try', 'catch', 'finally', 'break', 'continue', 'Type', 'Prop', 'Sort', 'local', 'deriving',
17
+ ]);
18
+ export function ident(name) {
19
+ return KEYWORDS.has(name) || !/^[A-Za-z_][A-Za-z0-9_']*$/.test(name) ? `«${name}»` : name;
20
+ }
21
+ export class Translator {
22
+ env;
23
+ fresh = 0;
24
+ constructor(env) {
25
+ this.env = env;
26
+ }
27
+ resolve(t) {
28
+ return resolvePub(t, this.env.layout.pub);
29
+ }
30
+ type(t) {
31
+ const env = this.env;
32
+ switch (t.k) {
33
+ case 'unit': return 'Unit';
34
+ case 'bool':
35
+ case 'frame': return 'Bool';
36
+ case 'nat': return 'Nat';
37
+ case 'int': return 'Int';
38
+ case 'string': return 'String';
39
+ case 'node': return env.node;
40
+ case 'agent': return env.agent;
41
+ case 'move': return 'Move';
42
+ case 'verdict': return 'Verdict';
43
+ case 'pub': return env.pub;
44
+ case 'option': return `Option (${this.type(t.of)})`;
45
+ case 'list':
46
+ case 'multiset': return `List (${this.type(t.of)})`;
47
+ case 'enum':
48
+ case 'record': return env.typeName(t);
49
+ default: throw new GenError(`no Lean type for ${showType(t)}`);
50
+ }
51
+ }
52
+ ctor(t, c) {
53
+ if (t.k === 'move')
54
+ return `Move.${c}`;
55
+ if (t.k === 'verdict')
56
+ return `Verdict.${c}`;
57
+ if (t.k === 'enum')
58
+ return `${this.env.typeName(t)}.${ident(c)}`;
59
+ throw new GenError(`no constructors for ${showType(t)}`);
60
+ }
61
+ lit(v, t) {
62
+ t = this.resolve(t);
63
+ switch (t.k) {
64
+ case 'unit': return '()';
65
+ case 'bool':
66
+ case 'frame': return String(v);
67
+ case 'nat': return String(v);
68
+ case 'int': return `(${v} : Int)`;
69
+ case 'string': return JSON.stringify(v);
70
+ case 'node': return `(Fin.ofNat ${this.env.N} ${v})`;
71
+ case 'move':
72
+ case 'verdict':
73
+ case 'enum': return this.ctor(t, v);
74
+ case 'option': return v === null ? `(none : ${this.type(t)})` : `(some ${this.lit(v, t.of)})`;
75
+ case 'list':
76
+ case 'multiset': return `([${v.map(x => this.lit(x, t.of)).join(', ')}] : ${this.type(t)})`;
77
+ case 'record': {
78
+ const o = v;
79
+ return `({ ${t.fields.map(f => `${ident(f.name)} := ${this.lit(o[f.name], f.type)}`).join(', ')} } : ${this.type(t)})`;
80
+ }
81
+ default: throw new GenError(`no literals of type ${showType(t)}`);
82
+ }
83
+ }
84
+ name(prefix) {
85
+ return `_${prefix}${++this.fresh}`;
86
+ }
87
+ /** The code of `tr` as a term of type `ND`. */
88
+ nd(tr) {
89
+ return tr.nd ? tr.code : `(pure ${wrap(tr.code)})`;
90
+ }
91
+ /** Evaluates `parts` (in order) and combines their values; binds those with choices. */
92
+ seq(parts, t, build) {
93
+ if (parts.every(p => !p.nd))
94
+ return { code: build(parts.map(p => p.code)), t, nd: false };
95
+ const binds = [];
96
+ const codes = parts.map(p => {
97
+ if (!p.nd)
98
+ return p.code;
99
+ const n = this.name('v');
100
+ binds.push(`let ${n} ← ${p.code}`);
101
+ return n;
102
+ });
103
+ return { code: `(do ${binds.join('; ')}; pure ${wrap(build(codes))})`, t, nd: true };
104
+ }
105
+ root(e, vars, config) {
106
+ const scope = { vars: new Map(vars.map(([n, code, t]) => [n, { code, t: this.resolve(t) }])), config };
107
+ return this.tr(e, scope);
108
+ }
109
+ tr(e, scope) {
110
+ const env = this.env;
111
+ switch (e.e) {
112
+ case 'lit':
113
+ return { code: this.lit(e.value, e.type), t: this.resolve(e.type), nd: false };
114
+ case 'var': {
115
+ const v = scope.vars.get(e.name) ?? fail(`unknown variable ${e.name}`);
116
+ return { code: v.code, t: v.t, nd: false };
117
+ }
118
+ case 'param':
119
+ return { code: env.param(e.name), t: env.paramType(e.name), nd: false };
120
+ case 'let': {
121
+ const v = this.tr(e.value, scope);
122
+ const n = ident(e.name);
123
+ const inner = { vars: new Map(scope.vars).set(e.name, { code: n, t: v.t }), config: v.t.k === 'config' ? n : scope.config };
124
+ const b = this.tr(e.body, inner);
125
+ if (!v.nd)
126
+ return { code: `(let ${n} := ${v.code}; ${b.code})`, t: b.t, nd: b.nd };
127
+ return { code: `(do let ${n} ← ${v.code}; ${this.nd(b)})`, t: b.t, nd: true };
128
+ }
129
+ case 'if': {
130
+ const [c, a, b] = [this.tr(e.cond, scope), this.tr(e.then, scope), this.tr(e.else, scope)];
131
+ if (!c.nd && !a.nd && !b.nd)
132
+ return { code: `(if ${c.code} then ${a.code} else ${b.code})`, t: a.t, nd: false };
133
+ const body = (cond) => `if ${cond} then ${this.nd(a)} else ${this.nd(b)}`;
134
+ if (!c.nd)
135
+ return { code: `(${body(c.code)})`, t: a.t, nd: true };
136
+ const n = this.name('c');
137
+ return { code: `(do let ${n} ← ${c.code}; ${body(n)})`, t: a.t, nd: true };
138
+ }
139
+ case 'match': {
140
+ const on = this.tr(e.on, scope);
141
+ const cases = Object.entries(e.cases).map(([c, b]) => [c, this.tr(b, scope)]);
142
+ const other = e.else ? this.tr(e.else, scope) : undefined;
143
+ const branches = [...cases.map(([, b]) => b), ...(other ? [other] : [])];
144
+ const nd = branches.some(b => b.nd);
145
+ const code = (x) => `(match ${x} with ${cases.map(([c, b]) => `| ${this.ctor(on.t, c)} => ${nd ? this.nd(b) : b.code}`).join(' ')}${other ? ` | _ => ${nd ? this.nd(other) : other.code}` : ''})`;
146
+ if (!on.nd)
147
+ return { code: code(on.code), t: branches[0].t, nd };
148
+ const n = this.name('m');
149
+ return { code: `(do let ${n} ← ${on.code}; ${code(n)})`, t: branches[0].t, nd: true };
150
+ }
151
+ case 'fn':
152
+ return fail('a function is only allowed as an argument of a builtin');
153
+ case 'record': {
154
+ const names = Object.keys(e.fields);
155
+ const parts = names.map(n => this.tr(e.fields[n], scope));
156
+ const t = T.record(names.map((name, i) => ({ name, type: parts[i].t })));
157
+ return this.seq(parts, t, cs => `({ ${names.map((n, i) => `${ident(n)} := ${cs[i]}`).join(', ')} } : ${this.type(t)})`);
158
+ }
159
+ case 'with': {
160
+ const on = this.tr(e.on, scope);
161
+ const names = Object.keys(e.fields);
162
+ const parts = [on, ...names.map(n => this.tr(e.fields[n], scope))];
163
+ return this.seq(parts, on.t, ([o, ...cs]) => `{ ${wrap(o)} with ${names.map((n, i) => `${ident(n)} := ${cs[i]}`).join(', ')} }`);
164
+ }
165
+ case 'get':
166
+ return this.get(e, scope);
167
+ case 'choose': {
168
+ const from = this.tr(e.from, scope);
169
+ if (!isCollection(from.t))
170
+ fail('choose: expected a collection');
171
+ const such = e.such ? this.lambda(e.such, [from.t.of], scope).code : undefined;
172
+ const site = JSON.stringify(e.site);
173
+ const pick = (l) => such ? `ND.chooseSuch ${site} ${wrap(l)} ${such}` : `ND.choose ${site} ${wrap(l)}`;
174
+ if (!from.nd)
175
+ return { code: `(${pick(from.code)})`, t: from.t.of, nd: true };
176
+ const n = this.name('l');
177
+ return { code: `(do let ${n} ← ${from.code}; ${pick(n)})`, t: from.t.of, nd: true };
178
+ }
179
+ case 'effect': {
180
+ if (!env.effect)
181
+ fail('effects are only generated in components');
182
+ const world = Object.entries(e.world ?? {}).map(([n, x]) => [n, this.tr(x, scope)]);
183
+ const raises = Object.entries(e.raise ?? {}).map(([flag, x]) => {
184
+ if (x.e === 'fn')
185
+ return [flag, { pred: this.lambda(x, [T.agent], scope).code }];
186
+ return [flag, { set: this.tr(x, scope) }];
187
+ });
188
+ const parts = [...world.map(([, t]) => t), ...raises.flatMap(([, r]) => 'set' in r ? [r.set] : [])];
189
+ return this.seq(parts, T.effect, cs => {
190
+ let j = world.length;
191
+ return env.effect(world.map(([n], i) => [n, cs[i]]), raises.map(([flag, r]) => [flag, 'pred' in r ? { pred: r.pred } : { set: cs[j++] }]));
192
+ });
193
+ }
194
+ case 'call':
195
+ return BUILTINS[e.fn] ? this.builtin(e, scope) : this.callDef(e, scope);
196
+ }
197
+ }
198
+ get(e, scope) {
199
+ const env = this.env;
200
+ // `C.world.f`
201
+ if (e.on.e === 'get' && e.on.field === 'world') {
202
+ const C = this.tr(e.on.on, scope);
203
+ if (C.t.k === 'config') {
204
+ const f = env.layout.worldType.k === 'record' ? fieldType(env.layout.worldType, e.field) : undefined;
205
+ if (!f)
206
+ fail(`unknown world field ${e.field}`);
207
+ return this.seq([C], this.resolve(f), ([c]) => env.worldField(c, e.field));
208
+ }
209
+ }
210
+ const on = this.tr(e.on, scope);
211
+ switch (on.t.k) {
212
+ case 'record': {
213
+ const ft = fieldType(on.t, e.field) ?? fail(`${showType(on.t)} has no field ${e.field}`);
214
+ return this.seq([on], ft, ([o]) => `${wrap(o)}.${ident(e.field)}`);
215
+ }
216
+ case 'config':
217
+ if (e.field === 'agents')
218
+ return { code: `(List.finRange ${env.k})`, t: T.multiset(T.agent), nd: false };
219
+ return fail(`configurations have no field ${e.field} here`);
220
+ case 'agent': {
221
+ const C = scope.config ?? fail(`reading ${e.field} of an agent needs a configuration`);
222
+ const at = (a) => `${C}.agents[${a}]`;
223
+ const lay = env.layout;
224
+ if (e.field === 'pos')
225
+ return this.seq([on], T.node, ([a]) => `${at(a)}.pos`);
226
+ if (e.field === 'pub')
227
+ return this.seq([on], lay.pub, ([a]) => `${at(a)}.pub`);
228
+ if (e.field === 'mem')
229
+ return this.seq([on], lay.mem, ([a]) => `${at(a)}.mem`);
230
+ if (lay.flags.some(f => f.name === e.field))
231
+ return this.seq([on], T.bool, ([a]) => env.agentFlag(at(a), e.field));
232
+ const f = lay.agentFields.find(f => f.name === e.field) ?? fail(`agents have no field ${e.field}`);
233
+ return this.seq([on], f.type, ([a]) => env.agentField(at(a), e.field));
234
+ }
235
+ case 'decision': {
236
+ if (e.field === 'move' || e.field === 'decided') {
237
+ return this.seq([on], e.field === 'move' ? T.move : T.bool, ([d]) => `${wrap(d)}.${e.field}`);
238
+ }
239
+ const a = env.layout.actions.find(a => a.name === e.field) ?? fail(`decisions have no action ${e.field}`);
240
+ return this.seq([on], a.type, ([d]) => env.decisionAction(d, e.field));
241
+ }
242
+ default:
243
+ return fail(`${showType(on.t)} has no fields`);
244
+ }
245
+ }
246
+ /** A deterministic lambda, with typed parameters. */
247
+ lambda(e, params, scope) {
248
+ if (e.e !== 'fn')
249
+ fail('expected a function');
250
+ const vars = new Map(scope.vars);
251
+ const binders = e.params.map((p, i) => {
252
+ const t = this.resolve(params[i]);
253
+ const n = ident(p.name);
254
+ vars.set(p.name, { code: n, t });
255
+ return `(${n} : ${this.type(t)})`;
256
+ });
257
+ const body = this.tr(e.body, { vars, config: scope.config });
258
+ if (body.nd)
259
+ fail('a function passed to a builtin cannot contain choices');
260
+ return { code: `(fun ${binders.join(' ')} => ${body.code})`, ret: body.t };
261
+ }
262
+ callDef(e, scope) {
263
+ const def = this.env.defs.get(e.fn) ?? fail(`unknown function ${e.fn}`);
264
+ const args = e.args.map(a => this.tr(a, scope));
265
+ const ret = this.resolve(def.ret);
266
+ const call = (cs) => `(${this.env.defName(e.fn)} ${cs.map(wrap).join(' ')})`;
267
+ // Definitions are generated before their callers, which marks those with choices.
268
+ if (!def.nd)
269
+ return this.seq(args, ret, call);
270
+ return this.bindSeq(args, ret, call);
271
+ }
272
+ /** Like `seq`, when the combination is itself a term of type `ND`. */
273
+ bindSeq(parts, t, build) {
274
+ const binds = [];
275
+ const codes = parts.map(p => {
276
+ if (!p.nd)
277
+ return p.code;
278
+ const n = this.name('v');
279
+ binds.push(`let ${n} ← ${p.code}`);
280
+ return n;
281
+ });
282
+ return { code: binds.length ? `(do ${binds.join('; ')}; ${build(codes)})` : build(codes), t, nd: true };
283
+ }
284
+ builtin(e, scope) {
285
+ const env = this.env;
286
+ const sig = BUILTINS[e.fn];
287
+ const types = [];
288
+ const parts = [];
289
+ const lambdas = [];
290
+ e.args.forEach((arg, i) => {
291
+ const lam = sig.lambdas?.[i];
292
+ if (lam) {
293
+ const params = lam(types);
294
+ if (typeof params === 'string')
295
+ fail(params);
296
+ const l = this.lambda(arg, params, scope);
297
+ types.push(T.fn(params, l.ret));
298
+ lambdas.push(l.code);
299
+ }
300
+ else {
301
+ const p = this.tr(arg, scope);
302
+ types.push(p.t);
303
+ parts.push(p);
304
+ lambdas.push(undefined);
305
+ }
306
+ });
307
+ const ret = sig.ret(types);
308
+ if (typeof ret === 'string')
309
+ fail(ret);
310
+ const C = scope.config;
311
+ return this.seq(parts, ret, vals => {
312
+ let j = 0;
313
+ const a = lambdas.map(l => l ?? vals[j++]);
314
+ const w = a.map(wrap);
315
+ const t0 = types[0];
316
+ switch (e.fn) {
317
+ case 'not': return `(!${w[0]})`;
318
+ case 'and': return `(${w.join(' && ')})`;
319
+ case 'or': return `(${w.join(' || ')})`;
320
+ case 'implies': return `(!${w[0]} || ${w[1]})`;
321
+ case 'eq': return `(${w[0]} == ${w[1]})`;
322
+ case 'ne': return `(${w[0]} != ${w[1]})`;
323
+ case 'lt': return `(decide (${w[0]} < ${w[1]}))`;
324
+ case 'le': return `(decide (${w[0]} ≤ ${w[1]}))`;
325
+ case 'gt': return `(decide (${w[0]} > ${w[1]}))`;
326
+ case 'ge': return `(decide (${w[0]} ≥ ${w[1]}))`;
327
+ case 'add': return `(${w[0]} + ${w[1]})`;
328
+ case 'sub': return `(${w[0]} - ${w[1]})`;
329
+ case 'mul': return `(${w[0]} * ${w[1]})`;
330
+ case 'div': return `(${w[0]} / ${w[1]})`;
331
+ case 'mod': return `(${w[0]} % ${w[1]})`;
332
+ case 'min': return `(min ${w[0]} ${w[1]})`;
333
+ case 'max': return `(max ${w[0]} ${w[1]})`;
334
+ case 'toInt': return `(Int.ofNat ${w[0]})`;
335
+ case 'some': return `(some ${w[0]})`;
336
+ case 'isSome': return `${w[0]}.isSome`;
337
+ case 'isNone': return `${w[0]}.isNone`;
338
+ case 'getD': return `(${w[0]}.getD ${w[1]})`;
339
+ case 'length': return `${w[0]}.length`;
340
+ case 'isEmpty': return `${w[0]}.isEmpty`;
341
+ case 'filter': return `(${w[0]}.filter ${w[1]})`;
342
+ case 'any': return `(${w[0]}.any ${w[1]})`;
343
+ case 'all': return `(${w[0]}.all ${w[1]})`;
344
+ case 'count': return `(${w[0]}.countP ${w[1]})`;
345
+ case 'map': return `(${w[0]}.map ${w[1]})`;
346
+ case 'sum': return `${w[0]}.sum`;
347
+ case 'mem': return `(${w[1]}.contains ${w[0]})`;
348
+ case 'range': return `(List.range ${w[0]})`;
349
+ case 'toMultiset': return a[0];
350
+ case 'subsets':
351
+ case 'subsetsUpTo':
352
+ if (t0.k === 'multiset' && t0.of.k !== 'agent') {
353
+ fail(`${e.fn} of a multiset of ${showType(t0.of)}: equal elements are not merged in Lean yet`);
354
+ }
355
+ return e.fn === 'subsets' ? `(B.subsets ${w[0]})` : `(B.subsetsUpTo ${w[1]} ${w[0]})`;
356
+ case 'dedup': return `${w[0]}.eraseDups`;
357
+ case 'diff': return `(B.diff ${w[0]} ${w[1]})`;
358
+ case 'append': return `(${w[0]} ++ ${w[1]})`;
359
+ case 'list': return `[${a.join(', ')}]`;
360
+ case 'at': return `(${w[0]}[${a[1]}]!)`;
361
+ case 'ofFn': return `(B.ofFn ${w[0]} ${w[1]})`;
362
+ case 'agents': return `(List.finRange ${env.k})`;
363
+ case 'nodes': return `(List.finRange ${env.N})`;
364
+ case 'size': return env.N;
365
+ case 'correct': return `(FlagSet.correct ${C ?? fail('correct needs a configuration')}.agents[${a[0]}].flags)`;
366
+ case 'present': return `(FlagSet.present ${C ?? fail('present needs a configuration')}.agents[${a[0]}].flags)`;
367
+ case 'decisionOf': return `(${env.ds ?? fail('decisionOf outside beforeMove')} ${w[0]})`;
368
+ case 'target': return `(Ring.target ${w[0]} ${w[1]})`;
369
+ case 'advance': return `(Ring.advance ${w[0]} ${w[1]} ${w[2]})`;
370
+ case 'edgePresent': return `(Ring.canMove ${env.ts ?? fail('no topology state here')} ${w[0]} ${w[1]})`;
371
+ case 'offset': return `(Ring.offset ${w[0]} ${w[1]} ${w[2]})`;
372
+ case 'toGlobal': return `(toGlobal ${w[0]} ${w[1]})`;
373
+ case 'opposite': return `(Move.flip ${w[0]})`;
374
+ case 'frames': return '[false, true]';
375
+ case 'node': return `(Fin.ofNat ${env.N} ${w[0]})`;
376
+ default: return fail(`builtin ${e.fn} has no Lean translation`);
377
+ }
378
+ });
379
+ }
380
+ }
381
+ export function fail(msg) {
382
+ throw new GenError(msg);
383
+ }
384
+ /** Parenthesizes a term unless it is atomic. */
385
+ export function wrap(code) {
386
+ return /^[A-Za-z0-9_.'«»!]+$/.test(code) || /^\(.*\)$/s.test(code) && balanced(code.slice(1, -1)) ? code : `(${code})`;
387
+ }
388
+ function balanced(s) {
389
+ let depth = 0;
390
+ for (const ch of s) {
391
+ if (ch === '(')
392
+ depth++;
393
+ else if (ch === ')' && --depth < 0)
394
+ return false;
395
+ }
396
+ return depth === 0;
397
+ }
398
+ export { MOVES, VERDICTS, typeEq };
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@bramas/simulant-lean-gen",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "license": "CECILL-B",
6
+ "type": "module",
7
+ "dependencies": {
8
+ "@bramas/simulant-lang": "0.1.0",
9
+ "@bramas/simulant-engine": "0.1.0"
10
+ },
11
+ "exports": {
12
+ "./check-proofs": {
13
+ "types": "./src/check-proofs.ts",
14
+ "default": "./dist/check-proofs.js"
15
+ },
16
+ "./component": {
17
+ "types": "./src/component.ts",
18
+ "default": "./dist/component.js"
19
+ },
20
+ "./differential": {
21
+ "types": "./src/differential.ts",
22
+ "default": "./dist/differential.js"
23
+ },
24
+ "./library": {
25
+ "types": "./src/library.ts",
26
+ "default": "./dist/library.js"
27
+ },
28
+ "./model": {
29
+ "types": "./src/model.ts",
30
+ "default": "./dist/model.js"
31
+ },
32
+ "./names": {
33
+ "types": "./src/names.ts",
34
+ "default": "./dist/names.js"
35
+ },
36
+ "./translate": {
37
+ "types": "./src/translate.ts",
38
+ "default": "./dist/translate.js"
39
+ }
40
+ },
41
+ "publishConfig": {
42
+ "registry": "https://registry.npmjs.org",
43
+ "access": "public"
44
+ },
45
+ "files": [
46
+ "src",
47
+ "dist",
48
+ "!src/examples.ts",
49
+ "!dist/examples.js",
50
+ "LICENCE.txt"
51
+ ],
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/Bramas/simulant.git",
55
+ "directory": "packages/lean-gen"
56
+ }
6
57
  }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Automated checks of a proof written by a researcher or an AI (plan, Section 5.2):
3
+ *
4
+ * - the proof file proves the generated statement, by name, without restating it;
5
+ * - the proof file cannot change what the statement means: no axioms, no `opaque` or
6
+ * `implemented_by`, no new syntax (notations, macros, elaborators), no `sorry`, `admit` or
7
+ * `native_decide`. Auxiliary definitions are allowed: the statement is a constant of the
8
+ * generated model file, which Lean forbids to redefine, and the check below pins its type;
9
+ * - `#print axioms` shows only `propext`, `Classical.choice` and `Quot.sound`.
10
+ *
11
+ * npx tsx packages/lean-gen/src/check-proofs.ts Examples.CautiousPair nonVacuity:NonVacuity
12
+ */
13
+ import { execFileSync } from 'node:child_process';
14
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
15
+ import { tmpdir } from 'node:os';
16
+ import { join } from 'node:path';
17
+
18
+ /** A theorem of the proof file, and the generated statement it must prove (none for a lemma about the model). */
19
+ export type Claim = { theorem: string; statement?: string };
20
+
21
+ export type ProofCheck = { ok: boolean; problems: string[]; axioms: Record<string, string[]> };
22
+
23
+ const ALLOWED_AXIOMS = new Set(['propext', 'Classical.choice', 'Quot.sound']);
24
+
25
+ /** Commands a proof file may not contain. */
26
+ const FORBIDDEN = [
27
+ /^\s*(?:@\[[^\]]*\]\s*)?(?:private\s+|protected\s+|noncomputable\s+|partial\s+|unsafe\s+)*(axiom|opaque|notation|infix|infixl|infixr|prefix|postfix|macro|macro_rules|syntax|elab|elab_rules|declare_syntax_cat|initialize|builtin_initialize)\b/m,
28
+ /\bsorry\b/, /\badmit\b/, /\bnative_decide\b/, /implemented_by/, /\bextern\b/, /\bunsafe\b/,
29
+ ];
30
+
31
+ function stripComments(src: string): string {
32
+ return src.replace(/\/-[^]*?-\//g, '').replace(/--.*$/gm, '');
33
+ }
34
+
35
+ export function checkProofFile(source: string): string[] {
36
+ const text = stripComments(source);
37
+ const problems: string[] = [];
38
+ for (const re of FORBIDDEN) {
39
+ const m = text.match(re);
40
+ if (m) problems.push(`the proof file contains \`${m[0].trim()}\``);
41
+ }
42
+ return problems;
43
+ }
44
+
45
+ /** Checks the claims of a module `<Module>.Proofs` against `<Module>.Model`. */
46
+ export function checkProofs(leanDir: string, module: string, claims: Claim[]): ProofCheck {
47
+ const proofFile = join(leanDir, ...module.split('.'), 'Proofs.lean');
48
+ const problems = checkProofFile(readFileSync(proofFile, 'utf8'));
49
+ const dir = mkdtempSync(join(tmpdir(), 'simulant-proofs-'));
50
+ const file = join(dir, 'Check.lean');
51
+ writeFileSync(file, [
52
+ `import ${module}.Proofs`,
53
+ // The theorem must have the generated statement's type (up to definitional unfolding of nothing else).
54
+ ...claims.filter(c => c.statement).map(c => `example : ${module}.${c.statement} := ${module}.${c.theorem}`),
55
+ ...claims.map(c => `#print axioms ${module}.${c.theorem}`),
56
+ ].join('\n') + '\n');
57
+ const axioms: Record<string, string[]> = {};
58
+ try {
59
+ const out = execFileSync('lake', ['env', 'lean', file], { cwd: leanDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
60
+ for (const c of claims) {
61
+ const name = `${module}.${c.theorem}`;
62
+ const deps = out.match(new RegExp(`'${name.replace(/\./g, '\\.')}' depends on axioms: \\[([^\\]]*)\\]`));
63
+ axioms[c.theorem] = deps ? deps[1].split(',').map(s => s.trim()).filter(Boolean) : [];
64
+ for (const a of axioms[c.theorem]) {
65
+ if (!ALLOWED_AXIOMS.has(a)) problems.push(`${c.theorem} depends on axiom ${a}`);
66
+ }
67
+ }
68
+ } catch (error) {
69
+ const e = error as { stdout?: string; stderr?: string };
70
+ problems.push(`Lean rejected the check:\n${e.stdout ?? ''}${e.stderr ?? ''}`);
71
+ } finally {
72
+ rmSync(dir, { recursive: true, force: true });
73
+ }
74
+ return { ok: problems.length === 0, problems, axioms };
75
+ }
76
+
77
+ if (import.meta.url === `file://${process.argv[1]}`) {
78
+ const [module, ...specs] = process.argv.slice(2);
79
+ const claims = specs.map(s => { const [theorem, statement] = s.split(':'); return statement ? { theorem, statement } : { theorem }; });
80
+ const result = checkProofs(new URL('../../../lean/', import.meta.url).pathname, module, claims);
81
+ console.log(JSON.stringify(result, null, 1));
82
+ process.exit(result.ok ? 0 : 1);
83
+ }