lemmascript 0.1.0 → 0.3.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.
@@ -1,32 +1,25 @@
1
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.
2
+ * Dafny emitter — IR Dafny text.
6
3
  */
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;
4
+ // ── Ty → Dafny type string ─────────────────────────────────
5
+ function tyToDafny(ty) {
6
+ switch (ty.kind) {
7
+ case "nat": return "nat";
8
+ case "int": return "int";
9
+ case "real": return "real";
10
+ case "bool": return "bool";
11
+ case "string": return "string";
12
+ case "void": return "()";
13
+ case "array": return `seq<${tyToDafny(ty.elem)}>`;
14
+ case "map": return `map<${tyToDafny(ty.key)}, ${tyToDafny(ty.value)}>`;
15
+ case "set": return `set<${tyToDafny(ty.elem)}>`;
16
+ case "optional": {
17
+ needsOptionType = true;
18
+ return `Option<${tyToDafny(ty.inner)}>`;
19
+ }
20
+ case "user": return ty.name;
21
+ case "unknown": return "int";
22
+ }
30
23
  }
31
24
  // ── Dafny keyword escaping ──────────────────────────────────
32
25
  const DAFNY_KEYWORDS = new Set([
@@ -37,7 +30,7 @@ const DAFNY_KEYWORDS = new Set([
37
30
  "match", "case", "return", "break", "continue",
38
31
  "requires", "ensures", "invariant", "decreases",
39
32
  "forall", "exists", "old", "fresh", "allocated",
40
- "true", "false", "null", "this", "new",
33
+ "true", "false", "null", /*"this",*/ "new",
41
34
  "datatype", "type", "const", "ghost", "static",
42
35
  "reads", "modifies", "assert", "assume", "print",
43
36
  "by", "calc", "reveal",
@@ -52,56 +45,116 @@ function escapeName(name) {
52
45
  }
53
46
  /** Format a typed parameter list for Dafny: "x: int, y: seq<int>" */
54
47
  function paramList(params) {
55
- return params.map(p => `${escapeName(p.name)}: ${leanTypeToDafny(p.type)}`).join(", ");
48
+ return params.map(p => `${escapeName(p.name)}: ${tyToDafny(p.type)}`).join(", ");
56
49
  }
57
50
  // ── Lean op → Dafny op ─────────────────────────────────────
58
51
  const OP_MAP = {
59
52
  "=": "==", "≠": "!=", "≥": ">=", "≤": "<=",
60
53
  "∧": "&&", "∨": "||", "¬": "!",
54
+ "arrayConcat": "+",
61
55
  };
62
56
  function mapOp(op) { return OP_MAP[op] ?? op; }
63
57
  // ── Expression emission ─────────────────────────────────────
64
58
  function emitExpr(e) {
65
59
  switch (e.kind) {
66
- case "var": return escapeName(e.name);
60
+ case "var": return e.name === "undefined" ? "None" : escapeName(e.name);
67
61
  case "num": return `${e.value}`;
68
62
  case "bool": return e.value ? "true" : "false";
69
- case "str": return `"${e.value}"`;
63
+ case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
70
64
  case "constructor": return qualifyCtor(e.name, e.type);
71
65
  case "arrayLiteral":
72
66
  if (e.elems.length === 0)
73
67
  return `[]`;
74
68
  return `[${e.elems.map(emitExpr).join(", ")}]`;
75
- case "dotCall": {
69
+ case "emptyMap": return `map[]`;
70
+ case "emptySet": return `{}`;
71
+ case "methodCall": {
76
72
  const obj = emitExpr(e.obj);
77
73
  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})`;
74
+ const ty = e.objTy.kind;
75
+ // Array methods
76
+ if (ty === "array") {
77
+ if (e.method === "with")
78
+ return `${obj}[${args[0]} := ${args[1]}]`;
79
+ if (e.method === "includes")
80
+ return `(${args[0]} in ${obj})`;
81
+ if (e.method === "push")
82
+ return `(${obj} + [${args[0]}])`;
83
+ if (e.method === "concat")
84
+ return `(${obj} + [${args[0]}])`;
85
+ if (e.method === "slice" && args.length === 1)
86
+ return `${obj}[${args[0]}..]`;
87
+ if (e.method === "slice" && args.length === 2)
88
+ return `${obj}[${args[0]}..${args[1]}]`;
89
+ if (e.method === "map")
90
+ return `Std.Collections.Seq.Map(${args[0]}, ${obj})`;
91
+ if (e.method === "filter")
92
+ return `Std.Collections.Seq.Filter(${args[0]}, ${obj})`;
93
+ if (e.method === "every")
94
+ return `Std.Collections.Seq.All(${obj}, ${args[0]})`;
95
+ if (e.method === "some" && e.args[0].kind === "lambda" &&
96
+ e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
97
+ const lam = e.args[0];
98
+ const ret = lam.body[0];
99
+ if (ret.kind !== "return")
100
+ throw new Error("unreachable");
101
+ const p = escapeName(lam.params[0]?.name ?? "x");
102
+ const body = emitExpr(ret.value);
103
+ return `(exists ${p} :: ${p} in ${obj} && ${body})`;
104
+ }
85
105
  }
86
- if (e.method === "filter" && args.length === 1) {
87
- needsStdCollections = true;
88
- return `Seq.Filter(${args[0]}, ${obj})`;
106
+ // String methods
107
+ if (ty === "string") {
108
+ if (e.method === "indexOf") {
109
+ needsStringIndexOf = true;
110
+ return `StringIndexOf(${obj}, ${args[0]})`;
111
+ }
112
+ if (e.method === "slice")
113
+ return `${obj}[${args[0]}..${args[1]}]`;
114
+ if (e.method === "trim") {
115
+ needsStringTrim = true;
116
+ return `StringTrim(${obj})`;
117
+ }
118
+ if (e.method === "toLowerCase") {
119
+ needsStringToLower = true;
120
+ return `StringToLower(${obj})`;
121
+ }
122
+ if (e.method === "toUpperCase") {
123
+ needsStringToUpper = true;
124
+ return `StringToUpper(${obj})`;
125
+ }
126
+ if (e.method === "includes") {
127
+ needsStringIndexOf = true;
128
+ return `(StringIndexOf(${obj}, ${args[0]}) >= 0)`;
129
+ }
130
+ if (e.method === "charCodeAt")
131
+ return `(${obj}[${args[0]}] as int)`;
89
132
  }
90
- if (e.method === "every" && args.length === 1) {
91
- needsStdCollections = true;
92
- return `Seq.All(${obj}, ${args[0]})`;
133
+ // Map methods
134
+ if (ty === "map") {
135
+ if (e.method === "getDirect")
136
+ return `${obj}[${args[0]}]`;
137
+ if (e.method === "get") {
138
+ needsOptionType = true;
139
+ return `(if ${args[0]} in ${obj} then Some(${obj}[${args[0]}]) else None)`;
140
+ }
141
+ if (e.method === "set")
142
+ return `${obj}[${args[0]} := ${args[1]}]`;
143
+ if (e.method === "has")
144
+ return `(${args[0]} in ${obj})`;
145
+ if (e.method === "delete")
146
+ return `(map k | k in ${obj} && k != ${args[0]} :: ${obj}[k])`;
93
147
  }
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})`;
148
+ // Set methods
149
+ if (ty === "set") {
150
+ if (e.method === "has")
151
+ return `(${args[0]} in ${obj})`;
152
+ if (e.method === "add")
153
+ return `(${obj} + {${args[0]}})`;
154
+ if (e.method === "delete")
155
+ return `(${obj} - {${args[0]}})`;
103
156
  }
104
- return `${obj}.${e.method}(${args.join(", ")})`;
157
+ throw new Error(`Unsupported Dafny method call: .${e.method}() on ${ty}`);
105
158
  }
106
159
  case "lambda": {
107
160
  const ps = paramList(e.params);
@@ -127,6 +180,45 @@ function emitExpr(e) {
127
180
  const pred = `${emitExpr(e.left)}.${ctorName}?`;
128
181
  return op === "!=" ? `(!${pred})` : pred;
129
182
  }
183
+ // Bitwise operators on int: translate to arithmetic
184
+ // x >> n → x / 2^n (right shift)
185
+ // x << n → x * 2^n (left shift)
186
+ if (e.op === ">>") {
187
+ if (e.right.kind === "num") {
188
+ return `(${emitExpr(e.left)} / ${Math.pow(2, e.right.value)})`;
189
+ }
190
+ needsPow2 = true;
191
+ return `(${emitExpr(e.left)} / Pow2(${emitExpr(e.right)}))`;
192
+ }
193
+ if (e.op === "<<") {
194
+ if (e.right.kind === "num") {
195
+ return `(${emitExpr(e.left)} * ${Math.pow(2, e.right.value)})`;
196
+ }
197
+ needsPow2 = true;
198
+ return `(${emitExpr(e.left)} * Pow2(${emitExpr(e.right)}))`;
199
+ }
200
+ // x & mask → x % (mask + 1) for literal masks of form 2^n - 1, else BitAnd
201
+ if (e.op === "&") {
202
+ if (e.right.kind === "num") {
203
+ const mask = e.right.value;
204
+ const modulus = mask + 1;
205
+ if ((modulus & (modulus - 1)) === 0) {
206
+ return `(${emitExpr(e.left)} % ${modulus})`;
207
+ }
208
+ }
209
+ needsBitAnd = true;
210
+ return `BitAnd(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
211
+ }
212
+ // int * real coercion: wrap int side with "as real"
213
+ if (["+", "-", "*", "/"].includes(op)) {
214
+ const leftIsReal = e.left.kind === "num" && !Number.isInteger(e.left.value);
215
+ const rightIsReal = e.right.kind === "num" && !Number.isInteger(e.right.value);
216
+ if (leftIsReal !== rightIsReal) {
217
+ const left = leftIsReal ? emitExpr(e.left) : `(${emitExpr(e.left)} as real)`;
218
+ const right = rightIsReal ? emitExpr(e.right) : `(${emitExpr(e.right)} as real)`;
219
+ return `(${left} ${op} ${right})`;
220
+ }
221
+ }
130
222
  return `(${emitExpr(e.left)} ${op} ${emitExpr(e.right)})`;
131
223
  }
132
224
  case "implies": {
@@ -135,23 +227,37 @@ function emitExpr(e) {
135
227
  }
136
228
  case "app": {
137
229
  const args = e.args.map(emitExpr);
138
- // Dafny built-in translations
139
- if (e.fn === "StringIndexOf") {
140
- needsStringIndexOf = true;
141
- return `StringIndexOf(${args.join(", ")})`;
230
+ if (e.fn === "SetToSeq") {
231
+ needsSetToSeq = true;
232
+ return `SetToSeq(${args.join(", ")})`;
142
233
  }
143
- if (e.fn === "StringSlice")
144
- return `${args[0]}[${args[1]}..${args[2]}]`;
145
- if (e.fn === "SeqPush")
146
- return `(${args[0]} + [${args[1]}])`;
234
+ if (e.fn === "BigInt" || e.fn === "Number")
235
+ return args[0]; // identity: both map to int
236
+ // Set literal: {a, b, c}
237
+ if (e.fn === "SetLiteral")
238
+ return `{${args.join(", ")}}`;
147
239
  if (e.fn === "JSFloorDiv")
148
240
  needsJSFloorDiv = true;
149
- return `${e.fn}(${args.join(", ")})`;
241
+ if (e.fn === "CeilReal")
242
+ needsCeilReal = true;
243
+ if (e.fn === "FloorReal")
244
+ needsFloorReal = true;
245
+ if (e.fn === "NatToString")
246
+ needsNatToString = true;
247
+ if (e.fn === "MathAbs")
248
+ needsMathAbs = true;
249
+ if (e.fn === "MathMin")
250
+ needsMathMin = true;
251
+ if (e.fn === "MathMax")
252
+ needsMathMax = true;
253
+ return `${escapeName(e.fn)}(${args.join(", ")})`;
150
254
  }
151
255
  case "field": {
152
256
  const obj = emitExpr(e.obj);
153
- if (e.field === "size" || e.field === "length")
257
+ if (e.field === "size" || e.field === "length" || e.field === "collectionSize")
154
258
  return `|${obj}|`;
259
+ if (e.field === "keys")
260
+ return `${obj}.Keys`;
155
261
  if (e.field === "toNat")
156
262
  return obj;
157
263
  return `${obj}.${escapeName(e.field)}`;
@@ -166,21 +272,75 @@ function emitExpr(e) {
166
272
  const updates = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
167
273
  return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
168
274
  }
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)
275
+ // Match constructor by field names prefer exact match over first-field heuristic
276
+ let ctorName;
277
+ if (e.fields.length > 0) {
278
+ const fieldNames = new Set(e.fields.map(f => f.name));
279
+ for (const [name, fields] of _structureDecls) {
280
+ if (fields.length >= e.fields.length && fields.every(f => fieldNames.has(f.name) || f.type.kind === "optional")) {
281
+ ctorName = name;
282
+ break;
283
+ }
284
+ }
285
+ if (!ctorName)
286
+ ctorName = _recordCtors.get(e.fields[0].name);
287
+ }
288
+ if (ctorName) {
289
+ const structFields = _structureDecls.get(ctorName);
290
+ if (structFields && e.fields.length < structFields.length) {
291
+ // Pad missing fields: match by name, fill None for optional
292
+ const provided = new Map(e.fields.map(f => [f.name, f]));
293
+ const vals = structFields.map(sf => {
294
+ const f = provided.get(sf.name);
295
+ if (f)
296
+ return emitExpr(f.value);
297
+ if (sf.type.kind === "optional") {
298
+ needsOptionType = true;
299
+ return "None";
300
+ }
301
+ return `/* missing: ${sf.name} */`;
302
+ });
303
+ return `${ctorName}(${vals.join(", ")})`;
304
+ }
305
+ const vals = e.fields.map(f => emitExpr(f.value));
172
306
  return `${ctorName}(${vals.join(", ")})`;
307
+ }
308
+ const vals = e.fields.map(f => emitExpr(f.value));
173
309
  return `(${vals.join(", ")})`;
174
310
  }
175
311
  case "if":
176
312
  return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
177
313
  case "match": {
178
- const arms = e.arms.map(a => `case ${a.pattern.replace(/^\./, "")} => ${emitExpr(a.body)}`);
179
- return `match ${e.scrutinee} { ${arms.join(" ")} }`;
314
+ const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
315
+ const arms = e.arms.map(a => `case ${translatePattern(a.pattern)} => ${emitExpr(a.body)}`);
316
+ return `(match ${scrut} { ${arms.join(" ")} })`;
317
+ }
318
+ case "forall": {
319
+ // Collapse nested foralls: forall x :: forall y :: P → forall x, y :: P
320
+ const vars = [];
321
+ let body = e;
322
+ while (body.kind === "forall") {
323
+ const dty = tyToDafny(body.type);
324
+ const ann = dty === "string" ? "" : `: ${dty}`;
325
+ vars.push(`${body.var}${ann}`);
326
+ body = body.body;
327
+ }
328
+ return `forall ${vars.join(", ")} :: ${emitExpr(body)}`;
329
+ }
330
+ case "exists": {
331
+ // Collapse nested exists: exists x :: exists y :: P → exists x, y :: P
332
+ const vars = [];
333
+ let body = e;
334
+ while (body.kind === "exists") {
335
+ const dty = tyToDafny(body.type);
336
+ const ann = dty === "string" ? "" : `: ${dty}`;
337
+ vars.push(`${body.var}${ann}`);
338
+ body = body.body;
339
+ }
340
+ return `exists ${vars.join(", ")} :: ${emitExpr(body)}`;
180
341
  }
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
342
  case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
343
+ case "havoc": return "*";
184
344
  }
185
345
  }
186
346
  /** Emit a pure expression with indentation for if/match/let. */
@@ -190,7 +350,8 @@ function emitPureExpr(e, indent) {
190
350
  case "if":
191
351
  return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
192
352
  case "match": {
193
- const lines = [`${pad}match ${e.scrutinee} {`];
353
+ const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
354
+ const lines = [`${pad}match ${scrut} {`];
194
355
  for (const arm of e.arms) {
195
356
  lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
196
357
  lines.push(emitPureExpr(arm.body, indent + 2));
@@ -212,9 +373,23 @@ function emitStmt(s, indent) {
212
373
  const pad = " ".repeat(indent);
213
374
  switch (s.kind) {
214
375
  case "let":
376
+ // Record literal assigned to map type → emit as map[k := v, ...]
377
+ if (s.type.kind === "map" && s.value.kind === "record" && !s.value.spread) {
378
+ const entries = s.value.fields.map(f => `${emitExpr({ kind: "str", value: f.name })} := ${emitExpr(f.value)}`);
379
+ return `${pad}var ${escapeName(s.name)}: ${tyToDafny(resolveTy(s.type))} := map[${entries.join(", ")}];`;
380
+ }
381
+ if (s.value.kind === "havoc" || s.value.kind === "emptyMap" || s.value.kind === "emptySet" ||
382
+ (s.value.kind === "arrayLiteral" && s.value.elems.length === 0))
383
+ return `${pad}var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
215
384
  return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
216
385
  case "assign":
217
386
  return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
387
+ case "ghostLet":
388
+ return `${pad}ghost var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
389
+ case "ghostAssign":
390
+ return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
391
+ case "assert":
392
+ return `${pad}assert ${emitExpr(s.expr)};`;
218
393
  case "bind":
219
394
  // Monadic bind shouldn't appear in Dafny mode, emit as regular assign
220
395
  return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
@@ -240,7 +415,8 @@ function emitStmt(s, indent) {
240
415
  return out;
241
416
  }
242
417
  case "match": {
243
- const lines = [`${pad}match ${s.scrutinee} {`];
418
+ const scrut = typeof s.scrutinee === "string" ? escapeName(s.scrutinee) : emitExpr(s.scrutinee);
419
+ const lines = [`${pad}match ${scrut} {`];
244
420
  for (const arm of s.arms) {
245
421
  lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
246
422
  lines.push(emitStmts(arm.body, indent + 2));
@@ -280,18 +456,23 @@ function emitStmt(s, indent) {
280
456
  function emitDecl(d) {
281
457
  switch (d.kind) {
282
458
  case "inductive": {
459
+ const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
283
460
  const ctors = d.constructors.map(c => {
284
461
  if (c.fields.length === 0)
285
462
  return escapeName(c.name);
286
463
  return `${escapeName(c.name)}(${paramList(c.fields)})`;
287
464
  });
288
- return `datatype ${d.name} = ${ctors.join(" | ")}`;
465
+ return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
289
466
  }
290
467
  case "structure": {
291
468
  return `datatype ${d.name} = ${d.name}(${paramList(d.fields)})`;
292
469
  }
470
+ case "type-alias": {
471
+ return `type ${d.name} = ${tyToDafny(d.target)}`;
472
+ }
293
473
  case "def": {
294
- const lines = [`function ${d.name}(${paramList(d.params)}): ${leanTypeToDafny(d.returnType)}`];
474
+ const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
475
+ const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
295
476
  for (const r of d.requires)
296
477
  lines.push(` requires ${emitExpr(r)}`);
297
478
  lines.push(`{`);
@@ -300,7 +481,7 @@ function emitDecl(d) {
300
481
  // Companion lemma for ensures (proof target for LLM)
301
482
  if (d.ensures.length > 0) {
302
483
  lines.push("");
303
- lines.push(`lemma ${d.name}_ensures(${paramList(d.params)})`);
484
+ lines.push(`lemma ${d.name}_ensures${tp}(${paramList(d.params)})`);
304
485
  for (const r of d.requires)
305
486
  lines.push(` requires ${emitExpr(r)}`);
306
487
  for (const e of d.ensures)
@@ -311,7 +492,8 @@ function emitDecl(d) {
311
492
  return lines.join("\n");
312
493
  }
313
494
  case "method": {
314
- const lines = [`method ${d.name}(${paramList(d.params)}) returns (res: ${leanTypeToDafny(d.returnType)})`];
495
+ const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
496
+ const lines = [`method ${d.name}${tp}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
315
497
  for (const r of d.requires)
316
498
  lines.push(` requires ${emitExpr(r)}`);
317
499
  for (const e of d.ensures)
@@ -321,6 +503,29 @@ function emitDecl(d) {
321
503
  lines.push(`}`);
322
504
  return lines.join("\n");
323
505
  }
506
+ case "class": {
507
+ const lines = [`class ${d.name} {`];
508
+ for (const f of d.fields) {
509
+ lines.push(` var ${escapeName(f.name)}: ${tyToDafny(f.type)}`);
510
+ }
511
+ if (d.fields.length > 0 && d.methods.length > 0)
512
+ lines.push("");
513
+ for (const m of d.methods) {
514
+ lines.push(` method ${m.name}(${paramList(m.params)}) returns (res: ${tyToDafny(m.returnType)})`);
515
+ for (const r of m.requires)
516
+ lines.push(` requires ${emitExpr(r)}`);
517
+ for (const e of m.ensures)
518
+ lines.push(` ensures ${emitExpr(e)}`);
519
+ lines.push(` {`);
520
+ lines.push(emitStmts(m.body, 2));
521
+ lines.push(` }`);
522
+ }
523
+ lines.push(`}`);
524
+ return lines.join("\n");
525
+ }
526
+ case "const": {
527
+ return `const ${escapeName(d.name)}: ${tyToDafny(d.type)} := ${emitExpr(d.value)}`;
528
+ }
324
529
  case "namespace": {
325
530
  // Dafny doesn't need namespaces — flatten declarations
326
531
  return d.decls.map(emitDecl).join("\n\n");
@@ -330,8 +535,33 @@ function emitDecl(d) {
330
535
  // ── File emission ───────────────────────────────────────────
331
536
  // ── Preamble helpers ────────────────────────────────────────
332
537
  let needsStringIndexOf = false;
538
+ let needsStringTrim = false;
539
+ let needsStringToLower = false;
540
+ let needsStringToUpper = false;
333
541
  let needsJSFloorDiv = false;
334
- let needsStdCollections = false;
542
+ let needsCeilReal = false;
543
+ let needsFloorReal = false;
544
+ let needsOptionType = false;
545
+ let needsSetToSeq = false;
546
+ let needsBitAnd = false;
547
+ let needsPow2 = false;
548
+ let needsNatToString = false;
549
+ let needsMathAbs = false;
550
+ let needsMathMin = false;
551
+ let needsMathMax = false;
552
+ const POW2 = `function Pow2(n: int): int
553
+ requires n >= 0
554
+ decreases n
555
+ {
556
+ if n == 0 then 1 else 2 * Pow2(n - 1)
557
+ }`;
558
+ const BIT_AND = `function BitAnd(x: int, y: int): int
559
+ requires x >= 0 && y >= 0
560
+ decreases x
561
+ {
562
+ if x == 0 || y == 0 then 0
563
+ else 2 * BitAnd(x / 2, y / 2) + (if x % 2 == 1 && y % 2 == 1 then 1 else 0)
564
+ }`;
335
565
  const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
336
566
  requires b != 0
337
567
  {
@@ -342,6 +572,15 @@ const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
342
572
  if a <= 0 then (-a) / (-b)
343
573
  else -((a - 1) / (-b)) - 1
344
574
  }`;
575
+ const FLOOR_REAL = `function FloorReal(x: real): int
576
+ {
577
+ x.Floor
578
+ }`;
579
+ const CEIL_REAL = `function CeilReal(x: real): int
580
+ {
581
+ if x == (x.Floor as real) then x.Floor
582
+ else x.Floor + 1
583
+ }`;
345
584
  const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
346
585
  {
347
586
  StringIndexOfFrom(s, sub, 0)
@@ -354,38 +593,121 @@ function StringIndexOfFrom(s: string, sub: string, from: nat): int
354
593
  else if s[from..from + |sub|] == sub then from as int
355
594
  else StringIndexOfFrom(s, sub, from + 1)
356
595
  }`;
596
+ const STRING_TRIM = `function StringTrimLeft(s: string): string
597
+ ensures |StringTrimLeft(s)| <= |s|
598
+ ensures StringTrimLeft(s) == "" || (|StringTrimLeft(s)| > 0 && StringTrimLeft(s)[0] != ' ')
599
+ decreases |s|
600
+ {
601
+ if |s| == 0 then ""
602
+ else if s[0] == ' ' then StringTrimLeft(s[1..])
603
+ else s
604
+ }
605
+
606
+ function StringTrimRight(s: string): string
607
+ ensures |StringTrimRight(s)| <= |s|
608
+ decreases |s|
609
+ {
610
+ if |s| == 0 then ""
611
+ else if s[|s|-1] == ' ' then StringTrimRight(s[..|s|-1])
612
+ else s
613
+ }
614
+
615
+ function StringTrim(s: string): string
616
+ {
617
+ StringTrimRight(StringTrimLeft(s))
618
+ }`;
619
+ const STRING_TO_LOWER = `function StringToLower(s: string): string
620
+ ensures |StringToLower(s)| == |s|
621
+ decreases |s|
622
+ {
623
+ if |s| == 0 then ""
624
+ else
625
+ var c := s[0];
626
+ var lower := if 'A' <= c <= 'Z' then (c - 'A' + 'a') as char else c;
627
+ [lower] + StringToLower(s[1..])
628
+ }`;
629
+ const STRING_TO_UPPER = `function StringToUpper(s: string): string
630
+ ensures |StringToUpper(s)| == |s|
631
+ decreases |s|
632
+ {
633
+ if |s| == 0 then ""
634
+ else
635
+ var c := s[0];
636
+ var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
637
+ [upper] + StringToUpper(s[1..])
638
+ }`;
639
+ const MATH_MIN = `function MathMin(a: int, b: int): int { if a <= b then a else b }`;
640
+ const MATH_MAX = `function MathMax(a: int, b: int): int { if a >= b then a else b }`;
641
+ const NAT_TO_STRING = `function NatToString(n: nat): string
642
+ decreases n
643
+ {
644
+ var digit := ('0' as int + n % 10) as char;
645
+ if n < 10 then [digit]
646
+ else NatToString(n / 10) + [digit]
647
+ }`;
648
+ const MATH_ABS = `function MathAbs(x: int): nat { if x >= 0 then x else -x }`;
357
649
  // ── Constructor and record helpers ───────────────────────────
358
650
  let _recordCtors = new Map();
651
+ let _structureDecls = new Map();
652
+ let _declaredTypes = new Set();
359
653
  function buildRecordCtorMap(decls) {
360
654
  _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);
655
+ _structureDecls = new Map();
656
+ _declaredTypes = new Set();
657
+ function collectDecl(d) {
658
+ if (d.kind === "structure") {
659
+ _declaredTypes.add(d.name);
660
+ _structureDecls.set(d.name, d.fields);
661
+ if (d.fields.length > 0)
662
+ _recordCtors.set(d.fields[0].name, d.name);
663
+ }
664
+ if (d.kind === "inductive")
665
+ _declaredTypes.add(d.name);
666
+ if (d.kind === "type-alias")
667
+ _declaredTypes.add(d.name);
668
+ if (d.kind === "def")
669
+ _declaredTypes.add(d.name);
364
670
  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
- }
671
+ for (const inner of d.decls)
672
+ collectDecl(inner);
369
673
  }
674
+ for (const d of decls)
675
+ collectDecl(d);
676
+ }
677
+ /** Resolve a Ty to a Dafny-safe type, falling back to string for undeclared user types. */
678
+ function resolveTy(ty) {
679
+ if (ty.kind === "user" && !_declaredTypes.has(ty.name))
680
+ return { kind: "string" };
681
+ if (ty.kind === "optional")
682
+ return { kind: "optional", inner: resolveTy(ty.inner) };
683
+ if (ty.kind === "array")
684
+ return { kind: "array", elem: resolveTy(ty.elem) };
685
+ if (ty.kind === "map")
686
+ return { kind: "map", key: resolveTy(ty.key), value: resolveTy(ty.value) };
687
+ if (ty.kind === "set")
688
+ return { kind: "set", elem: resolveTy(ty.elem) };
689
+ return ty;
370
690
  }
371
691
  function qualifyCtor(name, type) {
372
692
  const rawName = name.replace(/^\./, "");
693
+ const mapped = CTOR_MAP[rawName] ?? escapeName(rawName);
373
694
  if (type)
374
- return `${type}.${escapeName(rawName)}`;
375
- return escapeName(rawName);
695
+ return `${type}.${mapped}`;
696
+ return mapped;
376
697
  }
377
698
  /** Translate a Lean match pattern to Dafny syntax.
378
699
  * ".ctorName field1 field2" → "ctorName(field1, field2)"
379
700
  * ".ctorName" → "ctorName"
380
701
  * "_" → "_"
381
702
  */
703
+ const CTOR_MAP = { "some": "Some", "none": "None" };
382
704
  function translatePattern(pattern) {
383
705
  if (pattern === "_")
384
706
  return "_";
385
707
  const m = pattern.match(/^\.(\w+)\s*(.*)$/);
386
708
  if (!m)
387
709
  return pattern;
388
- const ctorName = escapeName(m[1]);
710
+ const ctorName = CTOR_MAP[m[1]] ?? escapeName(m[1]);
389
711
  const fields = m[2].trim();
390
712
  if (!fields)
391
713
  return ctorName;
@@ -398,8 +720,20 @@ const PREAMBLES = {
398
720
  export function emitDafnyFile(file, tsFileName) {
399
721
  buildRecordCtorMap(file.decls);
400
722
  needsStringIndexOf = false;
723
+ needsStringTrim = false;
724
+ needsStringToLower = false;
725
+ needsStringToUpper = false;
401
726
  needsJSFloorDiv = false;
402
- needsStdCollections = false;
727
+ needsCeilReal = false;
728
+ needsFloorReal = false;
729
+ needsOptionType = false;
730
+ needsSetToSeq = false;
731
+ needsBitAnd = false;
732
+ needsPow2 = false;
733
+ needsNatToString = false;
734
+ needsMathAbs = false;
735
+ needsMathMin = false;
736
+ needsMathMax = false;
403
737
  // Collect pure def names so we can skip their method wrappers
404
738
  const pureDefs = new Set();
405
739
  for (const d of file.decls) {
@@ -423,7 +757,9 @@ export function emitDafnyFile(file, tsFileName) {
423
757
  }
424
758
  catch (e) {
425
759
  const name = "name" in decl ? decl.name : "unknown";
426
- console.error(`WARNING: skipping '${name}': ${e.message}`);
760
+ const msg = e.message;
761
+ console.error(`WARNING: skipping '${name}': ${msg}`);
762
+ declLines.push(`// LemmaScript: skipped ${name}`);
427
763
  skipped.push(name);
428
764
  }
429
765
  }
@@ -434,16 +770,82 @@ export function emitDafnyFile(file, tsFileName) {
434
770
  const lines = [];
435
771
  if (tsFileName)
436
772
  lines.push(`// Generated by lsc from ${tsFileName}`);
437
- if (needsStdCollections)
438
- lines.push("import Std.Collections.Seq");
773
+ if (needsOptionType) {
774
+ lines.push("");
775
+ lines.push("datatype Option<T> = None | Some(value: T)");
776
+ }
777
+ if (needsSetToSeq) {
778
+ lines.push("");
779
+ lines.push(`method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
780
+ ensures forall x :: x in s <==> x in res
781
+ ensures |res| == |s|
782
+ {
783
+ var remaining := s;
784
+ res := [];
785
+ while remaining != {}
786
+ invariant remaining <= s
787
+ invariant forall x :: x in res <==> (x in s && x !in remaining)
788
+ invariant |res| + |remaining| == |s|
789
+ decreases remaining
790
+ {
791
+ var x :| x in remaining;
792
+ res := res + [x];
793
+ remaining := remaining - {x};
794
+ }
795
+ }`);
796
+ }
797
+ if (needsPow2) {
798
+ lines.push("");
799
+ lines.push(POW2);
800
+ }
801
+ if (needsBitAnd) {
802
+ lines.push("");
803
+ lines.push(BIT_AND);
804
+ }
439
805
  if (needsJSFloorDiv) {
440
806
  lines.push("");
441
807
  lines.push(JS_FLOOR_DIV);
442
808
  }
809
+ if (needsCeilReal) {
810
+ lines.push("");
811
+ lines.push(CEIL_REAL);
812
+ }
813
+ if (needsFloorReal) {
814
+ lines.push("");
815
+ lines.push(FLOOR_REAL);
816
+ }
443
817
  if (needsStringIndexOf) {
444
818
  lines.push("");
445
819
  lines.push(PREAMBLES.StringIndexOf);
446
820
  }
821
+ if (needsStringTrim) {
822
+ lines.push("");
823
+ lines.push(STRING_TRIM);
824
+ }
825
+ if (needsStringToLower) {
826
+ lines.push("");
827
+ lines.push(STRING_TO_LOWER);
828
+ }
829
+ if (needsStringToUpper) {
830
+ lines.push("");
831
+ lines.push(STRING_TO_UPPER);
832
+ }
833
+ if (needsNatToString) {
834
+ lines.push("");
835
+ lines.push(NAT_TO_STRING);
836
+ }
837
+ if (needsMathAbs) {
838
+ lines.push("");
839
+ lines.push(MATH_ABS);
840
+ }
841
+ if (needsMathMin) {
842
+ lines.push("");
843
+ lines.push(MATH_MIN);
844
+ }
845
+ if (needsMathMax) {
846
+ lines.push("");
847
+ lines.push(MATH_MAX);
848
+ }
447
849
  lines.push(...declLines);
448
850
  return lines.join("\n") + "\n";
449
851
  }