lemmascript 0.0.1 → 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,449 @@
1
+ /**
2
+ * Dafny emitter — translates Lean IR to Dafny syntax on the fly.
3
+ *
4
+ * No separate Dafny IR. The shared transform produces Lean IR,
5
+ * and this emitter maps it to Dafny syntax.
6
+ */
7
+ // ── Lean type → Dafny type ──────────────────────────────────
8
+ function leanTypeToDafny(t) {
9
+ // Simple mappings
10
+ const MAP = {
11
+ "Int": "int", "Nat": "nat", "Bool": "bool",
12
+ "String": "string", "Unit": "()", "_": "int",
13
+ };
14
+ if (MAP[t])
15
+ return MAP[t];
16
+ // Array (X Y) → seq<...>
17
+ const arrParenMatch = t.match(/^Array\s+\((.+)\)$/);
18
+ if (arrParenMatch)
19
+ return `seq<${leanTypeToDafny(arrParenMatch[1])}>`;
20
+ // Array X → seq<X>
21
+ const arrMatch = t.match(/^Array\s+(.+)$/);
22
+ if (arrMatch)
23
+ return `seq<${leanTypeToDafny(arrMatch[1])}>`;
24
+ // Strip parens: (X) → X
25
+ const parenMatch = t.match(/^\((.+)\)$/);
26
+ if (parenMatch)
27
+ return leanTypeToDafny(parenMatch[1]);
28
+ // User types pass through
29
+ return t;
30
+ }
31
+ // ── Dafny keyword escaping ──────────────────────────────────
32
+ const DAFNY_KEYWORDS = new Set([
33
+ "seq", "set", "map", "multiset", "iset", "imap",
34
+ "var", "method", "function", "predicate", "lemma",
35
+ "class", "trait", "module", "import", "export",
36
+ "if", "then", "else", "while", "for", "in",
37
+ "match", "case", "return", "break", "continue",
38
+ "requires", "ensures", "invariant", "decreases",
39
+ "forall", "exists", "old", "fresh", "allocated",
40
+ "true", "false", "null", "this", "new",
41
+ "datatype", "type", "const", "ghost", "static",
42
+ "reads", "modifies", "assert", "assume", "print",
43
+ "by", "calc", "reveal",
44
+ ]);
45
+ function escapeName(name) {
46
+ if (DAFNY_KEYWORDS.has(name))
47
+ return `${name}_`;
48
+ // Dafny doesn't allow identifiers starting with _
49
+ if (name.startsWith("_"))
50
+ return `i${name}`;
51
+ return name;
52
+ }
53
+ /** Format a typed parameter list for Dafny: "x: int, y: seq<int>" */
54
+ function paramList(params) {
55
+ return params.map(p => `${escapeName(p.name)}: ${leanTypeToDafny(p.type)}`).join(", ");
56
+ }
57
+ // ── Lean op → Dafny op ─────────────────────────────────────
58
+ const OP_MAP = {
59
+ "=": "==", "≠": "!=", "≥": ">=", "≤": "<=",
60
+ "∧": "&&", "∨": "||", "¬": "!",
61
+ };
62
+ function mapOp(op) { return OP_MAP[op] ?? op; }
63
+ // ── Expression emission ─────────────────────────────────────
64
+ function emitExpr(e) {
65
+ switch (e.kind) {
66
+ case "var": return escapeName(e.name);
67
+ case "num": return `${e.value}`;
68
+ case "bool": return e.value ? "true" : "false";
69
+ case "str": return `"${e.value}"`;
70
+ case "constructor": return qualifyCtor(e.name, e.type);
71
+ case "arrayLiteral":
72
+ if (e.elems.length === 0)
73
+ return `[]`;
74
+ return `[${e.elems.map(emitExpr).join(", ")}]`;
75
+ case "dotCall": {
76
+ const obj = emitExpr(e.obj);
77
+ const args = e.args.map(emitExpr);
78
+ if (e.method === "with" && args.length === 2)
79
+ return `${obj}[${args[0]} := ${args[1]}]`;
80
+ if (e.method === "includes" && args.length === 1)
81
+ return `(${args[0]} in ${obj})`;
82
+ if (e.method === "map" && args.length === 1) {
83
+ needsStdCollections = true;
84
+ return `Seq.Map(${args[0]}, ${obj})`;
85
+ }
86
+ if (e.method === "filter" && args.length === 1) {
87
+ needsStdCollections = true;
88
+ return `Seq.Filter(${args[0]}, ${obj})`;
89
+ }
90
+ if (e.method === "every" && args.length === 1) {
91
+ needsStdCollections = true;
92
+ return `Seq.All(${obj}, ${args[0]})`;
93
+ }
94
+ if (e.method === "some" && args.length === 1 && e.args[0].kind === "lambda" &&
95
+ e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
96
+ const lam = e.args[0];
97
+ const ret = lam.body[0];
98
+ if (ret.kind !== "return")
99
+ throw new Error("unreachable");
100
+ const p = escapeName(lam.params[0]?.name ?? "x");
101
+ const body = emitExpr(ret.value);
102
+ return `(exists ${p} :: ${p} in ${obj} && ${body})`;
103
+ }
104
+ return `${obj}.${e.method}(${args.join(", ")})`;
105
+ }
106
+ case "lambda": {
107
+ const ps = paramList(e.params);
108
+ if (e.body.length === 1 && e.body[0].kind === "return")
109
+ return `(${ps}) => ${emitExpr(e.body[0].value)}`;
110
+ throw new Error("Unsupported: multi-statement lambda in Dafny");
111
+ }
112
+ case "unop": {
113
+ const op = mapOp(e.op);
114
+ if (op === "!" && e.expr.kind !== "var" && e.expr.kind !== "bool")
115
+ return `!(${emitExpr(e.expr)})`;
116
+ if (e.op === "-" && e.expr.kind === "num")
117
+ return `(-(${e.expr.value}))`;
118
+ if (e.op === "-")
119
+ return `(-(${emitExpr(e.expr)}))`;
120
+ return `${op}(${emitExpr(e.expr)})`;
121
+ }
122
+ case "binop": {
123
+ // Discriminant check: x == .Ctor → x.Ctor?
124
+ const op = mapOp(e.op);
125
+ if ((op === "==" || op === "!=") && e.right.kind === "constructor") {
126
+ const ctorName = escapeName(e.right.name.replace(/^\./, ""));
127
+ const pred = `${emitExpr(e.left)}.${ctorName}?`;
128
+ return op === "!=" ? `(!${pred})` : pred;
129
+ }
130
+ return `(${emitExpr(e.left)} ${op} ${emitExpr(e.right)})`;
131
+ }
132
+ case "implies": {
133
+ const parts = [...e.premises.map(emitExpr), emitExpr(e.conclusion)];
134
+ return `(${parts.join(" ==> ")})`;
135
+ }
136
+ case "app": {
137
+ const args = e.args.map(emitExpr);
138
+ // Dafny built-in translations
139
+ if (e.fn === "StringIndexOf") {
140
+ needsStringIndexOf = true;
141
+ return `StringIndexOf(${args.join(", ")})`;
142
+ }
143
+ if (e.fn === "StringSlice")
144
+ return `${args[0]}[${args[1]}..${args[2]}]`;
145
+ if (e.fn === "SeqPush")
146
+ return `(${args[0]} + [${args[1]}])`;
147
+ if (e.fn === "JSFloorDiv")
148
+ needsJSFloorDiv = true;
149
+ return `${e.fn}(${args.join(", ")})`;
150
+ }
151
+ case "field": {
152
+ const obj = emitExpr(e.obj);
153
+ if (e.field === "size" || e.field === "length")
154
+ return `|${obj}|`;
155
+ if (e.field === "toNat")
156
+ return obj;
157
+ return `${obj}.${escapeName(e.field)}`;
158
+ }
159
+ case "toNat":
160
+ // Dafny doesn't need toNat — just emit the inner expression
161
+ return emitExpr(e.expr);
162
+ case "index":
163
+ return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]`;
164
+ case "record": {
165
+ if (e.spread) {
166
+ const updates = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
167
+ return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
168
+ }
169
+ const ctorName = e.fields.length > 0 ? _recordCtors.get(e.fields[0].name) : undefined;
170
+ const vals = e.fields.map(f => emitExpr(f.value));
171
+ if (ctorName)
172
+ return `${ctorName}(${vals.join(", ")})`;
173
+ return `(${vals.join(", ")})`;
174
+ }
175
+ case "if":
176
+ return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
177
+ case "match": {
178
+ const arms = e.arms.map(a => `case ${a.pattern.replace(/^\./, "")} => ${emitExpr(a.body)}`);
179
+ return `match ${e.scrutinee} { ${arms.join(" ")} }`;
180
+ }
181
+ case "forall": return `forall ${e.var}: ${leanTypeToDafny(e.type)} :: ${emitExpr(e.body)}`;
182
+ case "exists": return `exists ${e.var}: ${leanTypeToDafny(e.type)} :: ${emitExpr(e.body)}`;
183
+ case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
184
+ }
185
+ }
186
+ /** Emit a pure expression with indentation for if/match/let. */
187
+ function emitPureExpr(e, indent) {
188
+ const pad = " ".repeat(indent);
189
+ switch (e.kind) {
190
+ case "if":
191
+ return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
192
+ case "match": {
193
+ const lines = [`${pad}match ${e.scrutinee} {`];
194
+ for (const arm of e.arms) {
195
+ lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
196
+ lines.push(emitPureExpr(arm.body, indent + 2));
197
+ }
198
+ lines.push(`${pad}}`);
199
+ return lines.join("\n");
200
+ }
201
+ case "let":
202
+ return `${pad}var ${escapeName(e.name)} := ${emitExpr(e.value)};\n${emitPureExpr(e.body, indent)}`;
203
+ default:
204
+ return `${pad}${emitExpr(e)}`;
205
+ }
206
+ }
207
+ // ── Statement emission ──────────────────────────────────────
208
+ function emitStmts(stmts, indent) {
209
+ return stmts.map(s => emitStmt(s, indent)).join("\n");
210
+ }
211
+ function emitStmt(s, indent) {
212
+ const pad = " ".repeat(indent);
213
+ switch (s.kind) {
214
+ case "let":
215
+ return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
216
+ case "assign":
217
+ return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
218
+ case "bind":
219
+ // Monadic bind shouldn't appear in Dafny mode, emit as regular assign
220
+ return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
221
+ case "let-bind":
222
+ // Monadic let-bind shouldn't appear in Dafny mode, emit as regular let
223
+ return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
224
+ case "return":
225
+ return `${pad}return ${emitExpr(s.value)};`;
226
+ case "break":
227
+ return `${pad}break;`;
228
+ case "continue":
229
+ throw new Error("Unsupported Dafny construct: 'continue' statement");
230
+ case "if": {
231
+ let out = `${pad}if ${emitExpr(s.cond)} {\n${emitStmts(s.then, indent + 1)}\n${pad}}`;
232
+ if (s.else.length > 0) {
233
+ if (s.else.length === 1 && s.else[0].kind === "if") {
234
+ out += ` else ${emitStmt(s.else[0], indent).trimStart()}`;
235
+ }
236
+ else {
237
+ out += ` else {\n${emitStmts(s.else, indent + 1)}\n${pad}}`;
238
+ }
239
+ }
240
+ return out;
241
+ }
242
+ case "match": {
243
+ const lines = [`${pad}match ${s.scrutinee} {`];
244
+ for (const arm of s.arms) {
245
+ lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
246
+ lines.push(emitStmts(arm.body, indent + 2));
247
+ }
248
+ lines.push(`${pad}}`);
249
+ return lines.join("\n");
250
+ }
251
+ case "while": {
252
+ const lines = [`${pad}while ${emitExpr(s.cond)}`];
253
+ for (const inv of s.invariants)
254
+ lines.push(`${pad} invariant ${emitExpr(inv)}`);
255
+ if (s.decreasing)
256
+ lines.push(`${pad} decreases ${emitExpr(s.decreasing)}`);
257
+ lines.push(`${pad}{`);
258
+ lines.push(emitStmts(s.body, indent + 1));
259
+ lines.push(`${pad}}`);
260
+ return lines.join("\n");
261
+ }
262
+ case "forin": {
263
+ // Lean for-in → Dafny while loop over index
264
+ const idx = escapeName(s.idx);
265
+ const lines = [
266
+ `${pad}var ${idx} := 0;`,
267
+ `${pad}while ${idx} < ${emitExpr(s.bound)}`,
268
+ ];
269
+ for (const inv of s.invariants)
270
+ lines.push(`${pad} invariant ${emitExpr(inv)}`);
271
+ lines.push(`${pad}{`);
272
+ lines.push(emitStmts(s.body, indent + 1));
273
+ lines.push(`${pad} ${idx} := ${idx} + 1;`);
274
+ lines.push(`${pad}}`);
275
+ return lines.join("\n");
276
+ }
277
+ }
278
+ }
279
+ // ── Declaration emission ────────────────────────────────────
280
+ function emitDecl(d) {
281
+ switch (d.kind) {
282
+ case "inductive": {
283
+ const ctors = d.constructors.map(c => {
284
+ if (c.fields.length === 0)
285
+ return escapeName(c.name);
286
+ return `${escapeName(c.name)}(${paramList(c.fields)})`;
287
+ });
288
+ return `datatype ${d.name} = ${ctors.join(" | ")}`;
289
+ }
290
+ case "structure": {
291
+ return `datatype ${d.name} = ${d.name}(${paramList(d.fields)})`;
292
+ }
293
+ case "def": {
294
+ const lines = [`function ${d.name}(${paramList(d.params)}): ${leanTypeToDafny(d.returnType)}`];
295
+ for (const r of d.requires)
296
+ lines.push(` requires ${emitExpr(r)}`);
297
+ lines.push(`{`);
298
+ lines.push(emitPureExpr(d.body, 1));
299
+ lines.push(`}`);
300
+ // Companion lemma for ensures (proof target for LLM)
301
+ if (d.ensures.length > 0) {
302
+ lines.push("");
303
+ lines.push(`lemma ${d.name}_ensures(${paramList(d.params)})`);
304
+ for (const r of d.requires)
305
+ lines.push(` requires ${emitExpr(r)}`);
306
+ for (const e of d.ensures)
307
+ lines.push(` ensures ${emitExpr(e)}`);
308
+ lines.push(`{`);
309
+ lines.push(`}`);
310
+ }
311
+ return lines.join("\n");
312
+ }
313
+ case "method": {
314
+ const lines = [`method ${d.name}(${paramList(d.params)}) returns (res: ${leanTypeToDafny(d.returnType)})`];
315
+ for (const r of d.requires)
316
+ lines.push(` requires ${emitExpr(r)}`);
317
+ for (const e of d.ensures)
318
+ lines.push(` ensures ${emitExpr(e)}`);
319
+ lines.push(`{`);
320
+ lines.push(emitStmts(d.body, 1));
321
+ lines.push(`}`);
322
+ return lines.join("\n");
323
+ }
324
+ case "namespace": {
325
+ // Dafny doesn't need namespaces — flatten declarations
326
+ return d.decls.map(emitDecl).join("\n\n");
327
+ }
328
+ }
329
+ }
330
+ // ── File emission ───────────────────────────────────────────
331
+ // ── Preamble helpers ────────────────────────────────────────
332
+ let needsStringIndexOf = false;
333
+ let needsJSFloorDiv = false;
334
+ let needsStdCollections = false;
335
+ const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
336
+ requires b != 0
337
+ {
338
+ if b > 0 then
339
+ if a >= 0 then a / b
340
+ else -((-a - 1) / b) - 1
341
+ else
342
+ if a <= 0 then (-a) / (-b)
343
+ else -((a - 1) / (-b)) - 1
344
+ }`;
345
+ const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
346
+ {
347
+ StringIndexOfFrom(s, sub, 0)
348
+ }
349
+
350
+ function StringIndexOfFrom(s: string, sub: string, from: nat): int
351
+ decreases |s| - from
352
+ {
353
+ if from + |sub| > |s| then -1
354
+ else if s[from..from + |sub|] == sub then from as int
355
+ else StringIndexOfFrom(s, sub, from + 1)
356
+ }`;
357
+ // ── Constructor and record helpers ───────────────────────────
358
+ let _recordCtors = new Map();
359
+ function buildRecordCtorMap(decls) {
360
+ _recordCtors = new Map();
361
+ for (const d of decls) {
362
+ if (d.kind === "structure" && d.fields.length > 0)
363
+ _recordCtors.set(d.fields[0].name, d.name);
364
+ if (d.kind === "namespace")
365
+ for (const inner of d.decls) {
366
+ if (inner.kind === "structure" && inner.fields.length > 0)
367
+ _recordCtors.set(inner.fields[0].name, inner.name);
368
+ }
369
+ }
370
+ }
371
+ function qualifyCtor(name, type) {
372
+ const rawName = name.replace(/^\./, "");
373
+ if (type)
374
+ return `${type}.${escapeName(rawName)}`;
375
+ return escapeName(rawName);
376
+ }
377
+ /** Translate a Lean match pattern to Dafny syntax.
378
+ * ".ctorName field1 field2" → "ctorName(field1, field2)"
379
+ * ".ctorName" → "ctorName"
380
+ * "_" → "_"
381
+ */
382
+ function translatePattern(pattern) {
383
+ if (pattern === "_")
384
+ return "_";
385
+ const m = pattern.match(/^\.(\w+)\s*(.*)$/);
386
+ if (!m)
387
+ return pattern;
388
+ const ctorName = escapeName(m[1]);
389
+ const fields = m[2].trim();
390
+ if (!fields)
391
+ return ctorName;
392
+ const fieldNames = fields.split(/\s+/).map(escapeName);
393
+ return `${ctorName}(${fieldNames.join(", ")})`;
394
+ }
395
+ const PREAMBLES = {
396
+ StringIndexOf: STRING_INDEX_OF,
397
+ };
398
+ export function emitDafnyFile(file, tsFileName) {
399
+ buildRecordCtorMap(file.decls);
400
+ needsStringIndexOf = false;
401
+ needsJSFloorDiv = false;
402
+ needsStdCollections = false;
403
+ // Collect pure def names so we can skip their method wrappers
404
+ const pureDefs = new Set();
405
+ for (const d of file.decls) {
406
+ if (d.kind === "namespace") {
407
+ for (const inner of d.decls)
408
+ if (inner.kind === "def")
409
+ pureDefs.add(inner.name);
410
+ }
411
+ if (d.kind === "def")
412
+ pureDefs.add(d.name);
413
+ }
414
+ // Emit declarations
415
+ const declLines = [];
416
+ const skipped = [];
417
+ for (const decl of file.decls) {
418
+ if (decl.kind === "method" && pureDefs.has(decl.name))
419
+ continue;
420
+ try {
421
+ declLines.push("");
422
+ declLines.push(emitDecl(decl));
423
+ }
424
+ catch (e) {
425
+ const name = "name" in decl ? decl.name : "unknown";
426
+ console.error(`WARNING: skipping '${name}': ${e.message}`);
427
+ skipped.push(name);
428
+ }
429
+ }
430
+ if (skipped.length > 0) {
431
+ console.error(`WARNING: ${skipped.length} declaration(s) skipped: ${skipped.join(", ")}`);
432
+ }
433
+ // Build output with needed preambles
434
+ const lines = [];
435
+ if (tsFileName)
436
+ lines.push(`// Generated by lsc from ${tsFileName}`);
437
+ if (needsStdCollections)
438
+ lines.push("import Std.Collections.Seq");
439
+ if (needsJSFloorDiv) {
440
+ lines.push("");
441
+ lines.push(JS_FLOOR_DIV);
442
+ }
443
+ if (needsStringIndexOf) {
444
+ lines.push("");
445
+ lines.push(PREAMBLES.StringIndexOf);
446
+ }
447
+ lines.push(...declLines);
448
+ return lines.join("\n") + "\n";
449
+ }