lemmascript 0.1.0 → 0.2.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,7 +45,7 @@ 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 = {
@@ -66,42 +59,101 @@ function emitExpr(e) {
66
59
  case "var": return escapeName(e.name);
67
60
  case "num": return `${e.value}`;
68
61
  case "bool": return e.value ? "true" : "false";
69
- case "str": return `"${e.value}"`;
62
+ case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
70
63
  case "constructor": return qualifyCtor(e.name, e.type);
71
64
  case "arrayLiteral":
72
65
  if (e.elems.length === 0)
73
66
  return `[]`;
74
67
  return `[${e.elems.map(emitExpr).join(", ")}]`;
75
- case "dotCall": {
68
+ case "emptyMap": return `map[]`;
69
+ case "emptySet": return `{}`;
70
+ case "methodCall": {
76
71
  const obj = emitExpr(e.obj);
77
72
  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})`;
73
+ const ty = e.objTy.kind;
74
+ // Array methods
75
+ if (ty === "array") {
76
+ if (e.method === "with")
77
+ return `${obj}[${args[0]} := ${args[1]}]`;
78
+ if (e.method === "includes")
79
+ return `(${args[0]} in ${obj})`;
80
+ if (e.method === "push")
81
+ return `(${obj} + [${args[0]}])`;
82
+ if (e.method === "slice")
83
+ 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
+ }
96
+ if (e.method === "some" && e.args[0].kind === "lambda" &&
97
+ e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
98
+ const lam = e.args[0];
99
+ const ret = lam.body[0];
100
+ if (ret.kind !== "return")
101
+ throw new Error("unreachable");
102
+ const p = escapeName(lam.params[0]?.name ?? "x");
103
+ const body = emitExpr(ret.value);
104
+ return `(exists ${p} :: ${p} in ${obj} && ${body})`;
105
+ }
85
106
  }
86
- if (e.method === "filter" && args.length === 1) {
87
- needsStdCollections = true;
88
- return `Seq.Filter(${args[0]}, ${obj})`;
107
+ // String methods
108
+ if (ty === "string") {
109
+ if (e.method === "indexOf") {
110
+ needsStringIndexOf = true;
111
+ return `StringIndexOf(${obj}, ${args[0]})`;
112
+ }
113
+ if (e.method === "slice")
114
+ return `${obj}[${args[0]}..${args[1]}]`;
115
+ if (e.method === "trim") {
116
+ needsStringTrim = true;
117
+ return `StringTrim(${obj})`;
118
+ }
119
+ if (e.method === "toLowerCase") {
120
+ needsStringToLower = true;
121
+ return `StringToLower(${obj})`;
122
+ }
123
+ if (e.method === "toUpperCase") {
124
+ needsStringToUpper = true;
125
+ return `StringToUpper(${obj})`;
126
+ }
127
+ if (e.method === "includes") {
128
+ needsStringIndexOf = true;
129
+ return `(StringIndexOf(${obj}, ${args[0]}) >= 0)`;
130
+ }
131
+ if (e.method === "charCodeAt")
132
+ return `(${obj}[${args[0]}] as int)`;
89
133
  }
90
- if (e.method === "every" && args.length === 1) {
91
- needsStdCollections = true;
92
- return `Seq.All(${obj}, ${args[0]})`;
134
+ // Map methods
135
+ if (ty === "map") {
136
+ if (e.method === "getDirect")
137
+ return `${obj}[${args[0]}]`;
138
+ if (e.method === "get") {
139
+ needsOptionType = true;
140
+ return `(if ${args[0]} in ${obj} then Some(${obj}[${args[0]}]) else None)`;
141
+ }
142
+ if (e.method === "set")
143
+ return `${obj}[${args[0]} := ${args[1]}]`;
144
+ if (e.method === "has")
145
+ return `(${args[0]} in ${obj})`;
93
146
  }
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})`;
147
+ // Set methods
148
+ if (ty === "set") {
149
+ if (e.method === "has")
150
+ return `(${args[0]} in ${obj})`;
151
+ if (e.method === "add")
152
+ return `(${obj} + {${args[0]}})`;
153
+ if (e.method === "delete")
154
+ return `(${obj} - {${args[0]}})`;
103
155
  }
104
- return `${obj}.${e.method}(${args.join(", ")})`;
156
+ throw new Error(`Unsupported Dafny method call: .${e.method}() on ${ty}`);
105
157
  }
106
158
  case "lambda": {
107
159
  const ps = paramList(e.params);
@@ -127,6 +179,45 @@ function emitExpr(e) {
127
179
  const pred = `${emitExpr(e.left)}.${ctorName}?`;
128
180
  return op === "!=" ? `(!${pred})` : pred;
129
181
  }
182
+ // Bitwise operators on int: translate to arithmetic
183
+ // x >> n → x / 2^n (right shift)
184
+ // x << n → x * 2^n (left shift)
185
+ if (e.op === ">>") {
186
+ if (e.right.kind === "num") {
187
+ return `(${emitExpr(e.left)} / ${Math.pow(2, e.right.value)})`;
188
+ }
189
+ needsPow2 = true;
190
+ return `(${emitExpr(e.left)} / Pow2(${emitExpr(e.right)}))`;
191
+ }
192
+ if (e.op === "<<") {
193
+ if (e.right.kind === "num") {
194
+ return `(${emitExpr(e.left)} * ${Math.pow(2, e.right.value)})`;
195
+ }
196
+ needsPow2 = true;
197
+ return `(${emitExpr(e.left)} * Pow2(${emitExpr(e.right)}))`;
198
+ }
199
+ // x & mask → x % (mask + 1) for literal masks of form 2^n - 1, else BitAnd
200
+ if (e.op === "&") {
201
+ if (e.right.kind === "num") {
202
+ const mask = e.right.value;
203
+ const modulus = mask + 1;
204
+ if ((modulus & (modulus - 1)) === 0) {
205
+ return `(${emitExpr(e.left)} % ${modulus})`;
206
+ }
207
+ }
208
+ needsBitAnd = true;
209
+ return `BitAnd(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
210
+ }
211
+ // int * real coercion: wrap int side with "as real"
212
+ if (["+", "-", "*", "/"].includes(op)) {
213
+ const leftIsReal = e.left.kind === "num" && !Number.isInteger(e.left.value);
214
+ const rightIsReal = e.right.kind === "num" && !Number.isInteger(e.right.value);
215
+ if (leftIsReal !== rightIsReal) {
216
+ const left = leftIsReal ? emitExpr(e.left) : `(${emitExpr(e.left)} as real)`;
217
+ const right = rightIsReal ? emitExpr(e.right) : `(${emitExpr(e.right)} as real)`;
218
+ return `(${left} ${op} ${right})`;
219
+ }
220
+ }
130
221
  return `(${emitExpr(e.left)} ${op} ${emitExpr(e.right)})`;
131
222
  }
132
223
  case "implies": {
@@ -135,23 +226,26 @@ function emitExpr(e) {
135
226
  }
136
227
  case "app": {
137
228
  const args = e.args.map(emitExpr);
138
- // Dafny built-in translations
139
- if (e.fn === "StringIndexOf") {
140
- needsStringIndexOf = true;
141
- return `StringIndexOf(${args.join(", ")})`;
229
+ if (e.fn === "SetToSeq") {
230
+ needsSetToSeq = true;
231
+ return `SetToSeq(${args.join(", ")})`;
142
232
  }
143
- if (e.fn === "StringSlice")
144
- return `${args[0]}[${args[1]}..${args[2]}]`;
145
- if (e.fn === "SeqPush")
146
- return `(${args[0]} + [${args[1]}])`;
233
+ if (e.fn === "BigInt" || e.fn === "Number")
234
+ return args[0]; // identity: both map to int
147
235
  if (e.fn === "JSFloorDiv")
148
236
  needsJSFloorDiv = true;
237
+ if (e.fn === "CeilReal")
238
+ needsCeilReal = true;
239
+ if (e.fn === "FloorReal")
240
+ needsFloorReal = true;
149
241
  return `${e.fn}(${args.join(", ")})`;
150
242
  }
151
243
  case "field": {
152
244
  const obj = emitExpr(e.obj);
153
- if (e.field === "size" || e.field === "length")
245
+ if (e.field === "size" || e.field === "length" || e.field === "collectionSize")
154
246
  return `|${obj}|`;
247
+ if (e.field === "keys")
248
+ return `${obj}.Keys`;
155
249
  if (e.field === "toNat")
156
250
  return obj;
157
251
  return `${obj}.${escapeName(e.field)}`;
@@ -175,12 +269,36 @@ function emitExpr(e) {
175
269
  case "if":
176
270
  return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
177
271
  case "match": {
178
- const arms = e.arms.map(a => `case ${a.pattern.replace(/^\./, "")} => ${emitExpr(a.body)}`);
179
- return `match ${e.scrutinee} { ${arms.join(" ")} }`;
272
+ const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
273
+ const arms = e.arms.map(a => `case ${translatePattern(a.pattern)} => ${emitExpr(a.body)}`);
274
+ return `(match ${scrut} { ${arms.join(" ")} })`;
275
+ }
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)}`;
180
299
  }
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
300
  case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
301
+ case "havoc": return "*";
184
302
  }
185
303
  }
186
304
  /** Emit a pure expression with indentation for if/match/let. */
@@ -190,7 +308,8 @@ function emitPureExpr(e, indent) {
190
308
  case "if":
191
309
  return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
192
310
  case "match": {
193
- const lines = [`${pad}match ${e.scrutinee} {`];
311
+ const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
312
+ const lines = [`${pad}match ${scrut} {`];
194
313
  for (const arm of e.arms) {
195
314
  lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
196
315
  lines.push(emitPureExpr(arm.body, indent + 2));
@@ -212,9 +331,17 @@ function emitStmt(s, indent) {
212
331
  const pad = " ".repeat(indent);
213
332
  switch (s.kind) {
214
333
  case "let":
334
+ if (s.value.kind === "havoc")
335
+ return `${pad}var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
215
336
  return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
216
337
  case "assign":
217
338
  return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
339
+ case "ghostLet":
340
+ return `${pad}ghost var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
341
+ case "ghostAssign":
342
+ return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
343
+ case "assert":
344
+ return `${pad}assert ${emitExpr(s.expr)};`;
218
345
  case "bind":
219
346
  // Monadic bind shouldn't appear in Dafny mode, emit as regular assign
220
347
  return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
@@ -240,7 +367,8 @@ function emitStmt(s, indent) {
240
367
  return out;
241
368
  }
242
369
  case "match": {
243
- const lines = [`${pad}match ${s.scrutinee} {`];
370
+ const scrut = typeof s.scrutinee === "string" ? escapeName(s.scrutinee) : emitExpr(s.scrutinee);
371
+ const lines = [`${pad}match ${scrut} {`];
244
372
  for (const arm of s.arms) {
245
373
  lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
246
374
  lines.push(emitStmts(arm.body, indent + 2));
@@ -291,7 +419,7 @@ function emitDecl(d) {
291
419
  return `datatype ${d.name} = ${d.name}(${paramList(d.fields)})`;
292
420
  }
293
421
  case "def": {
294
- const lines = [`function ${d.name}(${paramList(d.params)}): ${leanTypeToDafny(d.returnType)}`];
422
+ const lines = [`function ${d.name}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
295
423
  for (const r of d.requires)
296
424
  lines.push(` requires ${emitExpr(r)}`);
297
425
  lines.push(`{`);
@@ -311,7 +439,7 @@ function emitDecl(d) {
311
439
  return lines.join("\n");
312
440
  }
313
441
  case "method": {
314
- const lines = [`method ${d.name}(${paramList(d.params)}) returns (res: ${leanTypeToDafny(d.returnType)})`];
442
+ const lines = [`method ${d.name}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
315
443
  for (const r of d.requires)
316
444
  lines.push(` requires ${emitExpr(r)}`);
317
445
  for (const e of d.ensures)
@@ -321,6 +449,29 @@ function emitDecl(d) {
321
449
  lines.push(`}`);
322
450
  return lines.join("\n");
323
451
  }
452
+ case "class": {
453
+ const lines = [`class ${d.name} {`];
454
+ for (const f of d.fields) {
455
+ lines.push(` var ${escapeName(f.name)}: ${tyToDafny(f.type)}`);
456
+ }
457
+ if (d.fields.length > 0 && d.methods.length > 0)
458
+ lines.push("");
459
+ for (const m of d.methods) {
460
+ lines.push(` method ${m.name}(${paramList(m.params)}) returns (res: ${tyToDafny(m.returnType)})`);
461
+ for (const r of m.requires)
462
+ lines.push(` requires ${emitExpr(r)}`);
463
+ for (const e of m.ensures)
464
+ lines.push(` ensures ${emitExpr(e)}`);
465
+ lines.push(` {`);
466
+ lines.push(emitStmts(m.body, 2));
467
+ lines.push(` }`);
468
+ }
469
+ lines.push(`}`);
470
+ return lines.join("\n");
471
+ }
472
+ case "const": {
473
+ return `const ${escapeName(d.name)}: ${tyToDafny(d.type)} := ${emitExpr(d.value)}`;
474
+ }
324
475
  case "namespace": {
325
476
  // Dafny doesn't need namespaces — flatten declarations
326
477
  return d.decls.map(emitDecl).join("\n\n");
@@ -330,8 +481,30 @@ function emitDecl(d) {
330
481
  // ── File emission ───────────────────────────────────────────
331
482
  // ── Preamble helpers ────────────────────────────────────────
332
483
  let needsStringIndexOf = false;
484
+ let needsStringTrim = false;
485
+ let needsStringToLower = false;
486
+ let needsStringToUpper = false;
333
487
  let needsJSFloorDiv = false;
488
+ let needsCeilReal = false;
489
+ let needsFloorReal = false;
334
490
  let needsStdCollections = false;
491
+ let needsOptionType = false;
492
+ let needsSetToSeq = false;
493
+ let needsBitAnd = false;
494
+ let needsPow2 = false;
495
+ const POW2 = `function Pow2(n: int): int
496
+ requires n >= 0
497
+ decreases n
498
+ {
499
+ if n == 0 then 1 else 2 * Pow2(n - 1)
500
+ }`;
501
+ const BIT_AND = `function BitAnd(x: int, y: int): int
502
+ requires x >= 0 && y >= 0
503
+ decreases x
504
+ {
505
+ if x == 0 || y == 0 then 0
506
+ else 2 * BitAnd(x / 2, y / 2) + (if x % 2 == 1 && y % 2 == 1 then 1 else 0)
507
+ }`;
335
508
  const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
336
509
  requires b != 0
337
510
  {
@@ -342,6 +515,15 @@ const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
342
515
  if a <= 0 then (-a) / (-b)
343
516
  else -((a - 1) / (-b)) - 1
344
517
  }`;
518
+ const FLOOR_REAL = `function FloorReal(x: real): int
519
+ {
520
+ x.Floor
521
+ }`;
522
+ const CEIL_REAL = `function CeilReal(x: real): int
523
+ {
524
+ if x == (x.Floor as real) then x.Floor
525
+ else x.Floor + 1
526
+ }`;
345
527
  const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
346
528
  {
347
529
  StringIndexOfFrom(s, sub, 0)
@@ -354,6 +536,49 @@ function StringIndexOfFrom(s: string, sub: string, from: nat): int
354
536
  else if s[from..from + |sub|] == sub then from as int
355
537
  else StringIndexOfFrom(s, sub, from + 1)
356
538
  }`;
539
+ const STRING_TRIM = `function StringTrimLeft(s: string): string
540
+ ensures |StringTrimLeft(s)| <= |s|
541
+ ensures StringTrimLeft(s) == "" || (|StringTrimLeft(s)| > 0 && StringTrimLeft(s)[0] != ' ')
542
+ decreases |s|
543
+ {
544
+ if |s| == 0 then ""
545
+ else if s[0] == ' ' then StringTrimLeft(s[1..])
546
+ else s
547
+ }
548
+
549
+ function StringTrimRight(s: string): string
550
+ ensures |StringTrimRight(s)| <= |s|
551
+ decreases |s|
552
+ {
553
+ if |s| == 0 then ""
554
+ else if s[|s|-1] == ' ' then StringTrimRight(s[..|s|-1])
555
+ else s
556
+ }
557
+
558
+ function StringTrim(s: string): string
559
+ {
560
+ StringTrimRight(StringTrimLeft(s))
561
+ }`;
562
+ const STRING_TO_LOWER = `function StringToLower(s: string): string
563
+ ensures |StringToLower(s)| == |s|
564
+ decreases |s|
565
+ {
566
+ if |s| == 0 then ""
567
+ else
568
+ var c := s[0];
569
+ var lower := if 'A' <= c <= 'Z' then (c - 'A' + 'a') as char else c;
570
+ [lower] + StringToLower(s[1..])
571
+ }`;
572
+ const STRING_TO_UPPER = `function StringToUpper(s: string): string
573
+ ensures |StringToUpper(s)| == |s|
574
+ decreases |s|
575
+ {
576
+ if |s| == 0 then ""
577
+ else
578
+ var c := s[0];
579
+ var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
580
+ [upper] + StringToUpper(s[1..])
581
+ }`;
357
582
  // ── Constructor and record helpers ───────────────────────────
358
583
  let _recordCtors = new Map();
359
584
  function buildRecordCtorMap(decls) {
@@ -379,13 +604,14 @@ function qualifyCtor(name, type) {
379
604
  * ".ctorName" → "ctorName"
380
605
  * "_" → "_"
381
606
  */
607
+ const CTOR_MAP = { "some": "Some", "none": "None" };
382
608
  function translatePattern(pattern) {
383
609
  if (pattern === "_")
384
610
  return "_";
385
611
  const m = pattern.match(/^\.(\w+)\s*(.*)$/);
386
612
  if (!m)
387
613
  return pattern;
388
- const ctorName = escapeName(m[1]);
614
+ const ctorName = CTOR_MAP[m[1]] ?? escapeName(m[1]);
389
615
  const fields = m[2].trim();
390
616
  if (!fields)
391
617
  return ctorName;
@@ -398,8 +624,17 @@ const PREAMBLES = {
398
624
  export function emitDafnyFile(file, tsFileName) {
399
625
  buildRecordCtorMap(file.decls);
400
626
  needsStringIndexOf = false;
627
+ needsStringTrim = false;
628
+ needsStringToLower = false;
629
+ needsStringToUpper = false;
401
630
  needsJSFloorDiv = false;
631
+ needsCeilReal = false;
632
+ needsFloorReal = false;
402
633
  needsStdCollections = false;
634
+ needsOptionType = false;
635
+ needsSetToSeq = false;
636
+ needsBitAnd = false;
637
+ needsPow2 = false;
403
638
  // Collect pure def names so we can skip their method wrappers
404
639
  const pureDefs = new Set();
405
640
  for (const d of file.decls) {
@@ -423,7 +658,9 @@ export function emitDafnyFile(file, tsFileName) {
423
658
  }
424
659
  catch (e) {
425
660
  const name = "name" in decl ? decl.name : "unknown";
426
- console.error(`WARNING: skipping '${name}': ${e.message}`);
661
+ const msg = e.message;
662
+ console.error(`WARNING: skipping '${name}': ${msg}`);
663
+ declLines.push(`// LemmaScript: skipped ${name}`);
427
664
  skipped.push(name);
428
665
  }
429
666
  }
@@ -436,14 +673,66 @@ export function emitDafnyFile(file, tsFileName) {
436
673
  lines.push(`// Generated by lsc from ${tsFileName}`);
437
674
  if (needsStdCollections)
438
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
+ }
439
708
  if (needsJSFloorDiv) {
440
709
  lines.push("");
441
710
  lines.push(JS_FLOOR_DIV);
442
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
+ }
443
720
  if (needsStringIndexOf) {
444
721
  lines.push("");
445
722
  lines.push(PREAMBLES.StringIndexOf);
446
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);
735
+ }
447
736
  lines.push(...declLines);
448
737
  return lines.join("\n") + "\n";
449
738
  }