lemmascript 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemmascript",
3
- "version": "0.3.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
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;
@@ -55,6 +55,22 @@ const OP_MAP = {
55
55
  };
56
56
  function mapOp(op) { return OP_MAP[op] ?? op; }
57
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
+ }
58
74
  function emitExpr(e) {
59
75
  switch (e.kind) {
60
76
  case "var": return e.name === "undefined" ? "None" : escapeName(e.name);
@@ -106,25 +122,25 @@ function emitExpr(e) {
106
122
  // String methods
107
123
  if (ty === "string") {
108
124
  if (e.method === "indexOf") {
109
- needsStringIndexOf = true;
125
+ needPreamble("StringIndexOf");
110
126
  return `StringIndexOf(${obj}, ${args[0]})`;
111
127
  }
112
128
  if (e.method === "slice")
113
129
  return `${obj}[${args[0]}..${args[1]}]`;
114
130
  if (e.method === "trim") {
115
- needsStringTrim = true;
131
+ needPreamble("StringTrim");
116
132
  return `StringTrim(${obj})`;
117
133
  }
118
134
  if (e.method === "toLowerCase") {
119
- needsStringToLower = true;
135
+ needPreamble("StringToLower");
120
136
  return `StringToLower(${obj})`;
121
137
  }
122
138
  if (e.method === "toUpperCase") {
123
- needsStringToUpper = true;
139
+ needPreamble("StringToUpper");
124
140
  return `StringToUpper(${obj})`;
125
141
  }
126
142
  if (e.method === "includes") {
127
- needsStringIndexOf = true;
143
+ needPreamble("StringIndexOf");
128
144
  return `(StringIndexOf(${obj}, ${args[0]}) >= 0)`;
129
145
  }
130
146
  if (e.method === "charCodeAt")
@@ -135,7 +151,7 @@ function emitExpr(e) {
135
151
  if (e.method === "getDirect")
136
152
  return `${obj}[${args[0]}]`;
137
153
  if (e.method === "get") {
138
- needsOptionType = true;
154
+ needPreamble("OptionType");
139
155
  return `(if ${args[0]} in ${obj} then Some(${obj}[${args[0]}]) else None)`;
140
156
  }
141
157
  if (e.method === "set")
@@ -187,14 +203,14 @@ function emitExpr(e) {
187
203
  if (e.right.kind === "num") {
188
204
  return `(${emitExpr(e.left)} / ${Math.pow(2, e.right.value)})`;
189
205
  }
190
- needsPow2 = true;
206
+ needPreamble("Pow2");
191
207
  return `(${emitExpr(e.left)} / Pow2(${emitExpr(e.right)}))`;
192
208
  }
193
209
  if (e.op === "<<") {
194
210
  if (e.right.kind === "num") {
195
211
  return `(${emitExpr(e.left)} * ${Math.pow(2, e.right.value)})`;
196
212
  }
197
- needsPow2 = true;
213
+ needPreamble("Pow2");
198
214
  return `(${emitExpr(e.left)} * Pow2(${emitExpr(e.right)}))`;
199
215
  }
200
216
  // x & mask → x % (mask + 1) for literal masks of form 2^n - 1, else BitAnd
@@ -206,7 +222,7 @@ function emitExpr(e) {
206
222
  return `(${emitExpr(e.left)} % ${modulus})`;
207
223
  }
208
224
  }
209
- needsBitAnd = true;
225
+ needPreamble("BitAnd");
210
226
  return `BitAnd(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
211
227
  }
212
228
  // int * real coercion: wrap int side with "as real"
@@ -228,7 +244,7 @@ function emitExpr(e) {
228
244
  case "app": {
229
245
  const args = e.args.map(emitExpr);
230
246
  if (e.fn === "SetToSeq") {
231
- needsSetToSeq = true;
247
+ needPreamble("SetToSeq");
232
248
  return `SetToSeq(${args.join(", ")})`;
233
249
  }
234
250
  if (e.fn === "BigInt" || e.fn === "Number")
@@ -237,19 +253,19 @@ function emitExpr(e) {
237
253
  if (e.fn === "SetLiteral")
238
254
  return `{${args.join(", ")}}`;
239
255
  if (e.fn === "JSFloorDiv")
240
- needsJSFloorDiv = true;
256
+ needPreamble("JSFloorDiv");
241
257
  if (e.fn === "CeilReal")
242
- needsCeilReal = true;
258
+ needPreamble("CeilReal");
243
259
  if (e.fn === "FloorReal")
244
- needsFloorReal = true;
260
+ needPreamble("FloorReal");
245
261
  if (e.fn === "NatToString")
246
- needsNatToString = true;
262
+ needPreamble("NatToString");
247
263
  if (e.fn === "MathAbs")
248
- needsMathAbs = true;
264
+ needPreamble("MathAbs");
249
265
  if (e.fn === "MathMin")
250
- needsMathMin = true;
266
+ needPreamble("MathMin");
251
267
  if (e.fn === "MathMax")
252
- needsMathMax = true;
268
+ needPreamble("MathMax");
253
269
  return `${escapeName(e.fn)}(${args.join(", ")})`;
254
270
  }
255
271
  case "field": {
@@ -295,7 +311,7 @@ function emitExpr(e) {
295
311
  if (f)
296
312
  return emitExpr(f.value);
297
313
  if (sf.type.kind === "optional") {
298
- needsOptionType = true;
314
+ needPreamble("OptionType");
299
315
  return "None";
300
316
  }
301
317
  return `/* missing: ${sf.name} */`;
@@ -311,34 +327,12 @@ function emitExpr(e) {
311
327
  case "if":
312
328
  return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
313
329
  case "match": {
314
- const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
330
+ const scrut = emitScrutinee(e.scrutinee);
315
331
  const arms = e.arms.map(a => `case ${translatePattern(a.pattern)} => ${emitExpr(a.body)}`);
316
332
  return `(match ${scrut} { ${arms.join(" ")} })`;
317
333
  }
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)}`;
341
- }
334
+ case "forall": return emitQuantifier(e, "forall");
335
+ case "exists": return emitQuantifier(e, "exists");
342
336
  case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
343
337
  case "havoc": return "*";
344
338
  }
@@ -350,7 +344,7 @@ function emitPureExpr(e, indent) {
350
344
  case "if":
351
345
  return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
352
346
  case "match": {
353
- const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
347
+ const scrut = emitScrutinee(e.scrutinee);
354
348
  const lines = [`${pad}match ${scrut} {`];
355
349
  for (const arm of e.arms) {
356
350
  lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
@@ -415,7 +409,7 @@ function emitStmt(s, indent) {
415
409
  return out;
416
410
  }
417
411
  case "match": {
418
- const scrut = typeof s.scrutinee === "string" ? escapeName(s.scrutinee) : emitExpr(s.scrutinee);
412
+ const scrut = emitScrutinee(s.scrutinee);
419
413
  const lines = [`${pad}match ${scrut} {`];
420
414
  for (const arm of s.arms) {
421
415
  lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
@@ -480,8 +474,10 @@ function emitDecl(d) {
480
474
  lines.push(`}`);
481
475
  // Companion lemma for ensures (proof target for LLM)
482
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(", ")}>` : "";
483
479
  lines.push("");
484
- lines.push(`lemma ${d.name}_ensures${tp}(${paramList(d.params)})`);
480
+ lines.push(`lemma ${d.name}_ensures${lemmaTP}(${paramList(d.params)})`);
485
481
  for (const r of d.requires)
486
482
  lines.push(` requires ${emitExpr(r)}`);
487
483
  for (const e of d.ensures)
@@ -534,21 +530,9 @@ function emitDecl(d) {
534
530
  }
535
531
  // ── File emission ───────────────────────────────────────────
536
532
  // ── Preamble helpers ────────────────────────────────────────
537
- let needsStringIndexOf = false;
538
- let needsStringTrim = false;
539
- let needsStringToLower = false;
540
- let needsStringToUpper = false;
541
- let needsJSFloorDiv = 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;
533
+ /** Preamble tracking — emitters add keys via `needPreamble(key)`, emitDafnyFile emits them. */
534
+ const _neededPreambles = new Set();
535
+ function needPreamble(key) { _neededPreambles.add(key); }
552
536
  const POW2 = `function Pow2(n: int): int
553
537
  requires n >= 0
554
538
  decreases n
@@ -646,6 +630,41 @@ const NAT_TO_STRING = `function NatToString(n: nat): string
646
630
  else NatToString(n / 10) + [digit]
647
631
  }`;
648
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
+ ];
649
668
  // ── Constructor and record helpers ───────────────────────────
650
669
  let _recordCtors = new Map();
651
670
  let _structureDecls = new Map();
@@ -714,26 +733,9 @@ function translatePattern(pattern) {
714
733
  const fieldNames = fields.split(/\s+/).map(escapeName);
715
734
  return `${ctorName}(${fieldNames.join(", ")})`;
716
735
  }
717
- const PREAMBLES = {
718
- StringIndexOf: STRING_INDEX_OF,
719
- };
720
736
  export function emitDafnyFile(file, tsFileName) {
721
737
  buildRecordCtorMap(file.decls);
722
- needsStringIndexOf = false;
723
- needsStringTrim = false;
724
- needsStringToLower = false;
725
- needsStringToUpper = false;
726
- needsJSFloorDiv = 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;
738
+ _neededPreambles.clear();
737
739
  // Collect pure def names so we can skip their method wrappers
738
740
  const pureDefs = new Set();
739
741
  for (const d of file.decls) {
@@ -770,81 +772,11 @@ export function emitDafnyFile(file, tsFileName) {
770
772
  const lines = [];
771
773
  if (tsFileName)
772
774
  lines.push(`// Generated by lsc from ${tsFileName}`);
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
- }
805
- if (needsJSFloorDiv) {
806
- lines.push("");
807
- lines.push(JS_FLOOR_DIV);
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
- }
817
- if (needsStringIndexOf) {
818
- lines.push("");
819
- lines.push(PREAMBLES.StringIndexOf);
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);
775
+ for (const [key, code] of PREAMBLE_CODE) {
776
+ if (_neededPreambles.has(key)) {
777
+ lines.push("");
778
+ lines.push(code);
779
+ }
848
780
  }
849
781
  lines.push(...declLines);
850
782
  return lines.join("\n") + "\n";
@@ -558,7 +558,9 @@ function extractStmts(stmts) {
558
558
  const nameNode = decl?.getNameNode();
559
559
  if (nameNode && Node.isArrayBindingPattern(nameNode)) {
560
560
  for (const elem of nameNode.getElements()) {
561
- if (Node.isBindingElement(elem))
561
+ if (Node.isOmittedExpression(elem))
562
+ names.push("_");
563
+ else if (Node.isBindingElement(elem))
562
564
  names.push(elem.getNameNode().getText());
563
565
  }
564
566
  }
package/tools/dist/lsc.js CHANGED
@@ -33,6 +33,12 @@ function main() {
33
33
  timeLimit = parseInt(args[timeLimitIdx].split("=")[1]);
34
34
  args.splice(timeLimitIdx, 1);
35
35
  }
36
+ const extraFlagsIdx = args.findIndex(a => a.startsWith("--extra-flags="));
37
+ let extraFlags;
38
+ if (extraFlagsIdx >= 0) {
39
+ extraFlags = args[extraFlagsIdx].split("=").slice(1).join("=");
40
+ args.splice(extraFlagsIdx, 1);
41
+ }
36
42
  const [cmd, filePath] = args;
37
43
  if (!cmd || !filePath) {
38
44
  console.error("Usage: lsc <gen|check|regen|extract> [--backend=lean|dafny] <file.ts>");
@@ -101,7 +107,7 @@ function main() {
101
107
  dafnyGen(genPath, dfyPath, text);
102
108
  if (!dafnyCheckDiff(genPath, dfyPath))
103
109
  process.exit(1);
104
- if (!dafnyVerify(dfyPath, dir, timeLimit))
110
+ if (!dafnyVerify(dfyPath, dir, timeLimit, extraFlags))
105
111
  process.exit(1);
106
112
  return;
107
113
  }