lemmascript 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -16,8 +16,9 @@ See the external case studies:
16
16
  - **[colorwheel-lemmascript](https://github.com/midspiral/colorwheel-lemmascript/)** — verified color palette generator with mood + harmony constraints. 31 Lean proofs + 18 behavioral properties, 115 Dafny lemmas (invariant preservation, commutativity, NoOp completeness).
17
17
  - **[clear-split-lemmascript](https://github.com/midspiral/clear-split-lemmascript/)** — greenfield verified expense splitting web app. Conservation theorem, invariant preservation, delta laws — all proven in both Lean (no sorry) and Dafny (56 lemmas).
18
18
  - **[node-casbin-lemmascript](https://github.com/midspiral/node-casbin-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [node-casbin](https://github.com/casbin/node-casbin). 5 functions verified, 217 existing tests pass. End-to-end correctness and order independence for all 4 effect modes in both Lean and Dafny (39 lemmas).
19
- - **[hono-lemmascript](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [hono](https://github.com/honojs/hono)'s security middleware. Two CVEs verified: IP restriction bypass ([CVE-2026-39409](https://github.com/honojs/hono/security/advisories/GHSA-3mpf-rcc7-5347)) and cookie name bypass ([CVE-2026-39410](https://github.com/honojs/hono/security/advisories/GHSA-r5rp-j6wh-rvv4)) — 51 Dafny lemmas. Cookie verification done in-place. Dafny only.
20
- - **[charmchat](https://github.com/CHARM-BDF/charmchat/tree/lemma)** — in-progress verification of some sub-components, in Dafny.
19
+ - **[hono-lemmascript](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [hono](https://github.com/honojs/hono)'s security middleware. Two CVEs verified: IP restriction bypass ([CVE-2026-39409](https://github.com/honojs/hono/security/advisories/GHSA-3mpf-rcc7-5347)) and cookie name bypass ([CVE-2026-39410](https://github.com/honojs/hono/security/advisories/GHSA-r5rp-j6wh-rvv4)) — 51 Dafny lemmas. [Cookie verification done **in-place**](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/src/utils/cookie.ts#L79). Dafny only.
20
+ - **[charmchat](https://github.com/CHARM-BDF/charmchat/tree/lemma)** — brownfield verification of an AI agent orchestration backend. `isEmptyResult` (string emptiness predicate, 8 postconditions, <1s) and `topologicalSort` (Kahn's algorithm — memory safety, output bounds, completeness via acyclicity ranking witness, termination; 5 helper lemmas, 28 loop invariants). Dafny only.
21
+ - **[xyflow-lemmascript](https://github.com/midspiral/xyflow-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [xyflow](https://github.com/xyflow/xyflow)'s core edge and geometry utilities. 9 functions verified: `addEdge` (dedup — never loses edges, adds at most one), `reconnectEdge` (replace — bounded length), `connectionExists`, `getEdgeCenter` (midpoint correctness), `clamp` (bounds), `rectToBox`/`boxToRect` (field arithmetic), `getBoundsOfBoxes` (enclosure), `getOverlappingArea` (non-negative), `areSetsEqual` (subset + same size). 14 Dafny proof obligations. Dafny only.
21
22
 
22
23
  ## Setup
23
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemmascript",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,13 +31,14 @@ export function dafnyCheckDiff(genPath, dfyPath) {
31
31
  catch { /* diff not available */ }
32
32
  return true;
33
33
  }
34
- export function dafnyVerify(dfyPath, dir, timeLimit) {
34
+ export function dafnyVerify(dfyPath, dir, timeLimit, extraFlags) {
35
35
  console.log("Running dafny verify...");
36
36
  try {
37
37
  const content = readFileSync(dfyPath, "utf-8");
38
- const stdLibFlag = content.includes("import Std.") ? " --standard-libraries" : "";
38
+ const stdLibFlag = content.includes("Std.") ? " --standard-libraries" : "";
39
39
  const timeLimitFlag = timeLimit ? ` --verification-time-limit ${timeLimit}` : "";
40
- execSync(`dafny verify${stdLibFlag}${timeLimitFlag} "${dfyPath}"`, { cwd: dir, stdio: "inherit" });
40
+ const extra = extraFlags ? ` ${extraFlags}` : "";
41
+ execSync(`dafny verify${stdLibFlag}${timeLimitFlag}${extra} "${dfyPath}"`, { cwd: dir, stdio: "inherit" });
41
42
  return true;
42
43
  }
43
44
  catch {
@@ -14,7 +14,7 @@ function tyToDafny(ty) {
14
14
  case "map": return `map<${tyToDafny(ty.key)}, ${tyToDafny(ty.value)}>`;
15
15
  case "set": return `set<${tyToDafny(ty.elem)}>`;
16
16
  case "optional": {
17
- needsOptionType = true;
17
+ needPreamble("OptionType");
18
18
  return `Option<${tyToDafny(ty.inner)}>`;
19
19
  }
20
20
  case "user": return ty.name;
@@ -51,12 +51,29 @@ function paramList(params) {
51
51
  const OP_MAP = {
52
52
  "=": "==", "≠": "!=", "≥": ">=", "≤": "<=",
53
53
  "∧": "&&", "∨": "||", "¬": "!",
54
+ "arrayConcat": "+",
54
55
  };
55
56
  function mapOp(op) { return OP_MAP[op] ?? op; }
56
57
  // ── Expression emission ─────────────────────────────────────
58
+ /** Emit a match scrutinee — either a variable name (string) or an expression. */
59
+ function emitScrutinee(s) {
60
+ return typeof s === "string" ? escapeName(s) : emitExpr(s);
61
+ }
62
+ /** Collapse nested forall/exists into a single quantifier with multiple bound vars. */
63
+ function emitQuantifier(e, keyword) {
64
+ const vars = [];
65
+ let body = e;
66
+ while (body.kind === e.kind) {
67
+ const dty = tyToDafny(body.type);
68
+ const ann = dty === "string" ? "" : `: ${dty}`;
69
+ vars.push(`${body.var}${ann}`);
70
+ body = body.body;
71
+ }
72
+ return `${keyword} ${vars.join(", ")} :: ${emitExpr(body)}`;
73
+ }
57
74
  function emitExpr(e) {
58
75
  switch (e.kind) {
59
- case "var": return escapeName(e.name);
76
+ case "var": return e.name === "undefined" ? "None" : escapeName(e.name);
60
77
  case "num": return `${e.value}`;
61
78
  case "bool": return e.value ? "true" : "false";
62
79
  case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
@@ -79,20 +96,18 @@ function emitExpr(e) {
79
96
  return `(${args[0]} in ${obj})`;
80
97
  if (e.method === "push")
81
98
  return `(${obj} + [${args[0]}])`;
82
- if (e.method === "slice")
99
+ if (e.method === "concat")
100
+ return `(${obj} + [${args[0]}])`;
101
+ if (e.method === "slice" && args.length === 1)
83
102
  return `${obj}[${args[0]}..]`;
84
- if (e.method === "map") {
85
- needsStdCollections = true;
86
- return `Seq.Map(${args[0]}, ${obj})`;
87
- }
88
- if (e.method === "filter") {
89
- needsStdCollections = true;
90
- return `Seq.Filter(${args[0]}, ${obj})`;
91
- }
92
- if (e.method === "every") {
93
- needsStdCollections = true;
94
- return `Seq.All(${obj}, ${args[0]})`;
95
- }
103
+ if (e.method === "slice" && args.length === 2)
104
+ return `${obj}[${args[0]}..${args[1]}]`;
105
+ if (e.method === "map")
106
+ return `Std.Collections.Seq.Map(${args[0]}, ${obj})`;
107
+ if (e.method === "filter")
108
+ return `Std.Collections.Seq.Filter(${args[0]}, ${obj})`;
109
+ if (e.method === "every")
110
+ return `Std.Collections.Seq.All(${obj}, ${args[0]})`;
96
111
  if (e.method === "some" && e.args[0].kind === "lambda" &&
97
112
  e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
98
113
  const lam = e.args[0];
@@ -107,25 +122,25 @@ function emitExpr(e) {
107
122
  // String methods
108
123
  if (ty === "string") {
109
124
  if (e.method === "indexOf") {
110
- needsStringIndexOf = true;
125
+ needPreamble("StringIndexOf");
111
126
  return `StringIndexOf(${obj}, ${args[0]})`;
112
127
  }
113
128
  if (e.method === "slice")
114
129
  return `${obj}[${args[0]}..${args[1]}]`;
115
130
  if (e.method === "trim") {
116
- needsStringTrim = true;
131
+ needPreamble("StringTrim");
117
132
  return `StringTrim(${obj})`;
118
133
  }
119
134
  if (e.method === "toLowerCase") {
120
- needsStringToLower = true;
135
+ needPreamble("StringToLower");
121
136
  return `StringToLower(${obj})`;
122
137
  }
123
138
  if (e.method === "toUpperCase") {
124
- needsStringToUpper = true;
139
+ needPreamble("StringToUpper");
125
140
  return `StringToUpper(${obj})`;
126
141
  }
127
142
  if (e.method === "includes") {
128
- needsStringIndexOf = true;
143
+ needPreamble("StringIndexOf");
129
144
  return `(StringIndexOf(${obj}, ${args[0]}) >= 0)`;
130
145
  }
131
146
  if (e.method === "charCodeAt")
@@ -136,13 +151,15 @@ function emitExpr(e) {
136
151
  if (e.method === "getDirect")
137
152
  return `${obj}[${args[0]}]`;
138
153
  if (e.method === "get") {
139
- needsOptionType = true;
154
+ needPreamble("OptionType");
140
155
  return `(if ${args[0]} in ${obj} then Some(${obj}[${args[0]}]) else None)`;
141
156
  }
142
157
  if (e.method === "set")
143
158
  return `${obj}[${args[0]} := ${args[1]}]`;
144
159
  if (e.method === "has")
145
160
  return `(${args[0]} in ${obj})`;
161
+ if (e.method === "delete")
162
+ return `(map k | k in ${obj} && k != ${args[0]} :: ${obj}[k])`;
146
163
  }
147
164
  // Set methods
148
165
  if (ty === "set") {
@@ -186,14 +203,14 @@ function emitExpr(e) {
186
203
  if (e.right.kind === "num") {
187
204
  return `(${emitExpr(e.left)} / ${Math.pow(2, e.right.value)})`;
188
205
  }
189
- needsPow2 = true;
206
+ needPreamble("Pow2");
190
207
  return `(${emitExpr(e.left)} / Pow2(${emitExpr(e.right)}))`;
191
208
  }
192
209
  if (e.op === "<<") {
193
210
  if (e.right.kind === "num") {
194
211
  return `(${emitExpr(e.left)} * ${Math.pow(2, e.right.value)})`;
195
212
  }
196
- needsPow2 = true;
213
+ needPreamble("Pow2");
197
214
  return `(${emitExpr(e.left)} * Pow2(${emitExpr(e.right)}))`;
198
215
  }
199
216
  // x & mask → x % (mask + 1) for literal masks of form 2^n - 1, else BitAnd
@@ -205,7 +222,7 @@ function emitExpr(e) {
205
222
  return `(${emitExpr(e.left)} % ${modulus})`;
206
223
  }
207
224
  }
208
- needsBitAnd = true;
225
+ needPreamble("BitAnd");
209
226
  return `BitAnd(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
210
227
  }
211
228
  // int * real coercion: wrap int side with "as real"
@@ -227,18 +244,29 @@ function emitExpr(e) {
227
244
  case "app": {
228
245
  const args = e.args.map(emitExpr);
229
246
  if (e.fn === "SetToSeq") {
230
- needsSetToSeq = true;
247
+ needPreamble("SetToSeq");
231
248
  return `SetToSeq(${args.join(", ")})`;
232
249
  }
233
250
  if (e.fn === "BigInt" || e.fn === "Number")
234
251
  return args[0]; // identity: both map to int
252
+ // Set literal: {a, b, c}
253
+ if (e.fn === "SetLiteral")
254
+ return `{${args.join(", ")}}`;
235
255
  if (e.fn === "JSFloorDiv")
236
- needsJSFloorDiv = true;
256
+ needPreamble("JSFloorDiv");
237
257
  if (e.fn === "CeilReal")
238
- needsCeilReal = true;
258
+ needPreamble("CeilReal");
239
259
  if (e.fn === "FloorReal")
240
- needsFloorReal = true;
241
- return `${e.fn}(${args.join(", ")})`;
260
+ needPreamble("FloorReal");
261
+ if (e.fn === "NatToString")
262
+ needPreamble("NatToString");
263
+ if (e.fn === "MathAbs")
264
+ needPreamble("MathAbs");
265
+ if (e.fn === "MathMin")
266
+ needPreamble("MathMin");
267
+ if (e.fn === "MathMax")
268
+ needPreamble("MathMax");
269
+ return `${escapeName(e.fn)}(${args.join(", ")})`;
242
270
  }
243
271
  case "field": {
244
272
  const obj = emitExpr(e.obj);
@@ -260,43 +288,51 @@ function emitExpr(e) {
260
288
  const updates = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
261
289
  return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
262
290
  }
263
- const ctorName = e.fields.length > 0 ? _recordCtors.get(e.fields[0].name) : undefined;
264
- const vals = e.fields.map(f => emitExpr(f.value));
265
- if (ctorName)
291
+ // Match constructor by field names prefer exact match over first-field heuristic
292
+ let ctorName;
293
+ if (e.fields.length > 0) {
294
+ const fieldNames = new Set(e.fields.map(f => f.name));
295
+ for (const [name, fields] of _structureDecls) {
296
+ if (fields.length >= e.fields.length && fields.every(f => fieldNames.has(f.name) || f.type.kind === "optional")) {
297
+ ctorName = name;
298
+ break;
299
+ }
300
+ }
301
+ if (!ctorName)
302
+ ctorName = _recordCtors.get(e.fields[0].name);
303
+ }
304
+ if (ctorName) {
305
+ const structFields = _structureDecls.get(ctorName);
306
+ if (structFields && e.fields.length < structFields.length) {
307
+ // Pad missing fields: match by name, fill None for optional
308
+ const provided = new Map(e.fields.map(f => [f.name, f]));
309
+ const vals = structFields.map(sf => {
310
+ const f = provided.get(sf.name);
311
+ if (f)
312
+ return emitExpr(f.value);
313
+ if (sf.type.kind === "optional") {
314
+ needPreamble("OptionType");
315
+ return "None";
316
+ }
317
+ return `/* missing: ${sf.name} */`;
318
+ });
319
+ return `${ctorName}(${vals.join(", ")})`;
320
+ }
321
+ const vals = e.fields.map(f => emitExpr(f.value));
266
322
  return `${ctorName}(${vals.join(", ")})`;
323
+ }
324
+ const vals = e.fields.map(f => emitExpr(f.value));
267
325
  return `(${vals.join(", ")})`;
268
326
  }
269
327
  case "if":
270
328
  return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
271
329
  case "match": {
272
- const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
330
+ const scrut = emitScrutinee(e.scrutinee);
273
331
  const arms = e.arms.map(a => `case ${translatePattern(a.pattern)} => ${emitExpr(a.body)}`);
274
332
  return `(match ${scrut} { ${arms.join(" ")} })`;
275
333
  }
276
- case "forall": {
277
- // Collapse nested foralls: forall x :: forall y :: P → forall x, y :: P
278
- const vars = [];
279
- let body = e;
280
- while (body.kind === "forall") {
281
- const dty = tyToDafny(body.type);
282
- const ann = dty === "string" ? "" : `: ${dty}`;
283
- vars.push(`${body.var}${ann}`);
284
- body = body.body;
285
- }
286
- return `forall ${vars.join(", ")} :: ${emitExpr(body)}`;
287
- }
288
- case "exists": {
289
- // Collapse nested exists: exists x :: exists y :: P → exists x, y :: P
290
- const vars = [];
291
- let body = e;
292
- while (body.kind === "exists") {
293
- const dty = tyToDafny(body.type);
294
- const ann = dty === "string" ? "" : `: ${dty}`;
295
- vars.push(`${body.var}${ann}`);
296
- body = body.body;
297
- }
298
- return `exists ${vars.join(", ")} :: ${emitExpr(body)}`;
299
- }
334
+ case "forall": return emitQuantifier(e, "forall");
335
+ case "exists": return emitQuantifier(e, "exists");
300
336
  case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
301
337
  case "havoc": return "*";
302
338
  }
@@ -308,7 +344,7 @@ function emitPureExpr(e, indent) {
308
344
  case "if":
309
345
  return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
310
346
  case "match": {
311
- const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
347
+ const scrut = emitScrutinee(e.scrutinee);
312
348
  const lines = [`${pad}match ${scrut} {`];
313
349
  for (const arm of e.arms) {
314
350
  lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
@@ -331,7 +367,13 @@ function emitStmt(s, indent) {
331
367
  const pad = " ".repeat(indent);
332
368
  switch (s.kind) {
333
369
  case "let":
334
- if (s.value.kind === "havoc")
370
+ // Record literal assigned to map type → emit as map[k := v, ...]
371
+ if (s.type.kind === "map" && s.value.kind === "record" && !s.value.spread) {
372
+ const entries = s.value.fields.map(f => `${emitExpr({ kind: "str", value: f.name })} := ${emitExpr(f.value)}`);
373
+ return `${pad}var ${escapeName(s.name)}: ${tyToDafny(resolveTy(s.type))} := map[${entries.join(", ")}];`;
374
+ }
375
+ if (s.value.kind === "havoc" || s.value.kind === "emptyMap" || s.value.kind === "emptySet" ||
376
+ (s.value.kind === "arrayLiteral" && s.value.elems.length === 0))
335
377
  return `${pad}var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
336
378
  return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
337
379
  case "assign":
@@ -367,7 +409,7 @@ function emitStmt(s, indent) {
367
409
  return out;
368
410
  }
369
411
  case "match": {
370
- const scrut = typeof s.scrutinee === "string" ? escapeName(s.scrutinee) : emitExpr(s.scrutinee);
412
+ const scrut = emitScrutinee(s.scrutinee);
371
413
  const lines = [`${pad}match ${scrut} {`];
372
414
  for (const arm of s.arms) {
373
415
  lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
@@ -408,18 +450,23 @@ function emitStmt(s, indent) {
408
450
  function emitDecl(d) {
409
451
  switch (d.kind) {
410
452
  case "inductive": {
453
+ const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
411
454
  const ctors = d.constructors.map(c => {
412
455
  if (c.fields.length === 0)
413
456
  return escapeName(c.name);
414
457
  return `${escapeName(c.name)}(${paramList(c.fields)})`;
415
458
  });
416
- return `datatype ${d.name} = ${ctors.join(" | ")}`;
459
+ return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
417
460
  }
418
461
  case "structure": {
419
462
  return `datatype ${d.name} = ${d.name}(${paramList(d.fields)})`;
420
463
  }
464
+ case "type-alias": {
465
+ return `type ${d.name} = ${tyToDafny(d.target)}`;
466
+ }
421
467
  case "def": {
422
- const lines = [`function ${d.name}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
468
+ const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
469
+ const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
423
470
  for (const r of d.requires)
424
471
  lines.push(` requires ${emitExpr(r)}`);
425
472
  lines.push(`{`);
@@ -427,8 +474,10 @@ function emitDecl(d) {
427
474
  lines.push(`}`);
428
475
  // Companion lemma for ensures (proof target for LLM)
429
476
  if (d.ensures.length > 0) {
477
+ // Strip constraints like (==) from type params — ghost lemmas don't need them
478
+ const lemmaTP = d.typeParams.length > 0 ? `<${d.typeParams.map(t => t.replace(/\(.*\)/, '')).join(", ")}>` : "";
430
479
  lines.push("");
431
- lines.push(`lemma ${d.name}_ensures(${paramList(d.params)})`);
480
+ lines.push(`lemma ${d.name}_ensures${lemmaTP}(${paramList(d.params)})`);
432
481
  for (const r of d.requires)
433
482
  lines.push(` requires ${emitExpr(r)}`);
434
483
  for (const e of d.ensures)
@@ -439,7 +488,8 @@ function emitDecl(d) {
439
488
  return lines.join("\n");
440
489
  }
441
490
  case "method": {
442
- const lines = [`method ${d.name}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
491
+ const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
492
+ const lines = [`method ${d.name}${tp}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
443
493
  for (const r of d.requires)
444
494
  lines.push(` requires ${emitExpr(r)}`);
445
495
  for (const e of d.ensures)
@@ -480,18 +530,9 @@ function emitDecl(d) {
480
530
  }
481
531
  // ── File emission ───────────────────────────────────────────
482
532
  // ── Preamble helpers ────────────────────────────────────────
483
- let needsStringIndexOf = false;
484
- let needsStringTrim = false;
485
- let needsStringToLower = false;
486
- let needsStringToUpper = false;
487
- let needsJSFloorDiv = false;
488
- let needsCeilReal = false;
489
- let needsFloorReal = false;
490
- let needsStdCollections = false;
491
- let needsOptionType = false;
492
- let needsSetToSeq = false;
493
- let needsBitAnd = false;
494
- let needsPow2 = false;
533
+ /** Preamble tracking — emitters add keys via `needPreamble(key)`, emitDafnyFile emits them. */
534
+ const _neededPreambles = new Set();
535
+ function needPreamble(key) { _neededPreambles.add(key); }
495
536
  const POW2 = `function Pow2(n: int): int
496
537
  requires n >= 0
497
538
  decreases n
@@ -579,25 +620,99 @@ const STRING_TO_UPPER = `function StringToUpper(s: string): string
579
620
  var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
580
621
  [upper] + StringToUpper(s[1..])
581
622
  }`;
623
+ const MATH_MIN = `function MathMin(a: int, b: int): int { if a <= b then a else b }`;
624
+ const MATH_MAX = `function MathMax(a: int, b: int): int { if a >= b then a else b }`;
625
+ const NAT_TO_STRING = `function NatToString(n: nat): string
626
+ decreases n
627
+ {
628
+ var digit := ('0' as int + n % 10) as char;
629
+ if n < 10 then [digit]
630
+ else NatToString(n / 10) + [digit]
631
+ }`;
632
+ const MATH_ABS = `function MathAbs(x: int): nat { if x >= 0 then x else -x }`;
633
+ const SET_TO_SEQ = `method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
634
+ ensures forall x :: x in s <==> x in res
635
+ ensures |res| == |s|
636
+ {
637
+ var remaining := s;
638
+ res := [];
639
+ while remaining != {}
640
+ invariant remaining <= s
641
+ invariant forall x :: x in res <==> (x in s && x !in remaining)
642
+ invariant |res| + |remaining| == |s|
643
+ decreases remaining
644
+ {
645
+ var x :| x in remaining;
646
+ res := res + [x];
647
+ remaining := remaining - {x};
648
+ }
649
+ }`;
650
+ /** Preamble code keyed by name. Emitted in this order when needed. */
651
+ const PREAMBLE_CODE = [
652
+ ["OptionType", "datatype Option<T> = None | Some(value: T)"],
653
+ ["SetToSeq", SET_TO_SEQ],
654
+ ["Pow2", POW2],
655
+ ["BitAnd", BIT_AND],
656
+ ["JSFloorDiv", JS_FLOOR_DIV],
657
+ ["CeilReal", CEIL_REAL],
658
+ ["FloorReal", FLOOR_REAL],
659
+ ["StringIndexOf", STRING_INDEX_OF],
660
+ ["StringTrim", STRING_TRIM],
661
+ ["StringToLower", STRING_TO_LOWER],
662
+ ["StringToUpper", STRING_TO_UPPER],
663
+ ["NatToString", NAT_TO_STRING],
664
+ ["MathAbs", MATH_ABS],
665
+ ["MathMin", MATH_MIN],
666
+ ["MathMax", MATH_MAX],
667
+ ];
582
668
  // ── Constructor and record helpers ───────────────────────────
583
669
  let _recordCtors = new Map();
670
+ let _structureDecls = new Map();
671
+ let _declaredTypes = new Set();
584
672
  function buildRecordCtorMap(decls) {
585
673
  _recordCtors = new Map();
586
- for (const d of decls) {
587
- if (d.kind === "structure" && d.fields.length > 0)
588
- _recordCtors.set(d.fields[0].name, d.name);
674
+ _structureDecls = new Map();
675
+ _declaredTypes = new Set();
676
+ function collectDecl(d) {
677
+ if (d.kind === "structure") {
678
+ _declaredTypes.add(d.name);
679
+ _structureDecls.set(d.name, d.fields);
680
+ if (d.fields.length > 0)
681
+ _recordCtors.set(d.fields[0].name, d.name);
682
+ }
683
+ if (d.kind === "inductive")
684
+ _declaredTypes.add(d.name);
685
+ if (d.kind === "type-alias")
686
+ _declaredTypes.add(d.name);
687
+ if (d.kind === "def")
688
+ _declaredTypes.add(d.name);
589
689
  if (d.kind === "namespace")
590
- for (const inner of d.decls) {
591
- if (inner.kind === "structure" && inner.fields.length > 0)
592
- _recordCtors.set(inner.fields[0].name, inner.name);
593
- }
690
+ for (const inner of d.decls)
691
+ collectDecl(inner);
594
692
  }
693
+ for (const d of decls)
694
+ collectDecl(d);
695
+ }
696
+ /** Resolve a Ty to a Dafny-safe type, falling back to string for undeclared user types. */
697
+ function resolveTy(ty) {
698
+ if (ty.kind === "user" && !_declaredTypes.has(ty.name))
699
+ return { kind: "string" };
700
+ if (ty.kind === "optional")
701
+ return { kind: "optional", inner: resolveTy(ty.inner) };
702
+ if (ty.kind === "array")
703
+ return { kind: "array", elem: resolveTy(ty.elem) };
704
+ if (ty.kind === "map")
705
+ return { kind: "map", key: resolveTy(ty.key), value: resolveTy(ty.value) };
706
+ if (ty.kind === "set")
707
+ return { kind: "set", elem: resolveTy(ty.elem) };
708
+ return ty;
595
709
  }
596
710
  function qualifyCtor(name, type) {
597
711
  const rawName = name.replace(/^\./, "");
712
+ const mapped = CTOR_MAP[rawName] ?? escapeName(rawName);
598
713
  if (type)
599
- return `${type}.${escapeName(rawName)}`;
600
- return escapeName(rawName);
714
+ return `${type}.${mapped}`;
715
+ return mapped;
601
716
  }
602
717
  /** Translate a Lean match pattern to Dafny syntax.
603
718
  * ".ctorName field1 field2" → "ctorName(field1, field2)"
@@ -618,23 +733,9 @@ function translatePattern(pattern) {
618
733
  const fieldNames = fields.split(/\s+/).map(escapeName);
619
734
  return `${ctorName}(${fieldNames.join(", ")})`;
620
735
  }
621
- const PREAMBLES = {
622
- StringIndexOf: STRING_INDEX_OF,
623
- };
624
736
  export function emitDafnyFile(file, tsFileName) {
625
737
  buildRecordCtorMap(file.decls);
626
- needsStringIndexOf = false;
627
- needsStringTrim = false;
628
- needsStringToLower = false;
629
- needsStringToUpper = false;
630
- needsJSFloorDiv = false;
631
- needsCeilReal = false;
632
- needsFloorReal = false;
633
- needsStdCollections = false;
634
- needsOptionType = false;
635
- needsSetToSeq = false;
636
- needsBitAnd = false;
637
- needsPow2 = false;
738
+ _neededPreambles.clear();
638
739
  // Collect pure def names so we can skip their method wrappers
639
740
  const pureDefs = new Set();
640
741
  for (const d of file.decls) {
@@ -671,67 +772,11 @@ export function emitDafnyFile(file, tsFileName) {
671
772
  const lines = [];
672
773
  if (tsFileName)
673
774
  lines.push(`// Generated by lsc from ${tsFileName}`);
674
- if (needsStdCollections)
675
- lines.push("import Std.Collections.Seq");
676
- if (needsOptionType) {
677
- lines.push("");
678
- lines.push("datatype Option<T> = None | Some(value: T)");
679
- }
680
- if (needsSetToSeq) {
681
- lines.push("");
682
- lines.push(`method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
683
- ensures forall x :: x in s <==> x in res
684
- ensures |res| == |s|
685
- {
686
- var remaining := s;
687
- res := [];
688
- while remaining != {}
689
- invariant remaining <= s
690
- invariant forall x :: x in res <==> (x in s && x !in remaining)
691
- invariant |res| + |remaining| == |s|
692
- decreases remaining
693
- {
694
- var x :| x in remaining;
695
- res := res + [x];
696
- remaining := remaining - {x};
697
- }
698
- }`);
699
- }
700
- if (needsPow2) {
701
- lines.push("");
702
- lines.push(POW2);
703
- }
704
- if (needsBitAnd) {
705
- lines.push("");
706
- lines.push(BIT_AND);
707
- }
708
- if (needsJSFloorDiv) {
709
- lines.push("");
710
- lines.push(JS_FLOOR_DIV);
711
- }
712
- if (needsCeilReal) {
713
- lines.push("");
714
- lines.push(CEIL_REAL);
715
- }
716
- if (needsFloorReal) {
717
- lines.push("");
718
- lines.push(FLOOR_REAL);
719
- }
720
- if (needsStringIndexOf) {
721
- lines.push("");
722
- lines.push(PREAMBLES.StringIndexOf);
723
- }
724
- if (needsStringTrim) {
725
- lines.push("");
726
- lines.push(STRING_TRIM);
727
- }
728
- if (needsStringToLower) {
729
- lines.push("");
730
- lines.push(STRING_TO_LOWER);
731
- }
732
- if (needsStringToUpper) {
733
- lines.push("");
734
- lines.push(STRING_TO_UPPER);
775
+ for (const [key, code] of PREAMBLE_CODE) {
776
+ if (_neededPreambles.has(key)) {
777
+ lines.push("");
778
+ lines.push(code);
779
+ }
735
780
  }
736
781
  lines.push(...declLines);
737
782
  return lines.join("\n") + "\n";