lemmascript 0.3.0 → 0.3.2

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.2",
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);
@@ -78,6 +94,10 @@ function emitExpr(e) {
78
94
  return `${obj}[${args[0]} := ${args[1]}]`;
79
95
  if (e.method === "includes")
80
96
  return `(${args[0]} in ${obj})`;
97
+ if (e.method === "indexOf") {
98
+ needPreamble("SeqIndexOf");
99
+ return `SeqIndexOf(${obj}, ${args[0]})`;
100
+ }
81
101
  if (e.method === "push")
82
102
  return `(${obj} + [${args[0]}])`;
83
103
  if (e.method === "concat")
@@ -106,25 +126,25 @@ function emitExpr(e) {
106
126
  // String methods
107
127
  if (ty === "string") {
108
128
  if (e.method === "indexOf") {
109
- needsStringIndexOf = true;
129
+ needPreamble("StringIndexOf");
110
130
  return `StringIndexOf(${obj}, ${args[0]})`;
111
131
  }
112
132
  if (e.method === "slice")
113
133
  return `${obj}[${args[0]}..${args[1]}]`;
114
134
  if (e.method === "trim") {
115
- needsStringTrim = true;
135
+ needPreamble("StringTrim");
116
136
  return `StringTrim(${obj})`;
117
137
  }
118
138
  if (e.method === "toLowerCase") {
119
- needsStringToLower = true;
139
+ needPreamble("StringToLower");
120
140
  return `StringToLower(${obj})`;
121
141
  }
122
142
  if (e.method === "toUpperCase") {
123
- needsStringToUpper = true;
143
+ needPreamble("StringToUpper");
124
144
  return `StringToUpper(${obj})`;
125
145
  }
126
146
  if (e.method === "includes") {
127
- needsStringIndexOf = true;
147
+ needPreamble("StringIndexOf");
128
148
  return `(StringIndexOf(${obj}, ${args[0]}) >= 0)`;
129
149
  }
130
150
  if (e.method === "charCodeAt")
@@ -135,7 +155,7 @@ function emitExpr(e) {
135
155
  if (e.method === "getDirect")
136
156
  return `${obj}[${args[0]}]`;
137
157
  if (e.method === "get") {
138
- needsOptionType = true;
158
+ needPreamble("OptionType");
139
159
  return `(if ${args[0]} in ${obj} then Some(${obj}[${args[0]}]) else None)`;
140
160
  }
141
161
  if (e.method === "set")
@@ -187,14 +207,14 @@ function emitExpr(e) {
187
207
  if (e.right.kind === "num") {
188
208
  return `(${emitExpr(e.left)} / ${Math.pow(2, e.right.value)})`;
189
209
  }
190
- needsPow2 = true;
210
+ needPreamble("Pow2");
191
211
  return `(${emitExpr(e.left)} / Pow2(${emitExpr(e.right)}))`;
192
212
  }
193
213
  if (e.op === "<<") {
194
214
  if (e.right.kind === "num") {
195
215
  return `(${emitExpr(e.left)} * ${Math.pow(2, e.right.value)})`;
196
216
  }
197
- needsPow2 = true;
217
+ needPreamble("Pow2");
198
218
  return `(${emitExpr(e.left)} * Pow2(${emitExpr(e.right)}))`;
199
219
  }
200
220
  // x & mask → x % (mask + 1) for literal masks of form 2^n - 1, else BitAnd
@@ -206,7 +226,7 @@ function emitExpr(e) {
206
226
  return `(${emitExpr(e.left)} % ${modulus})`;
207
227
  }
208
228
  }
209
- needsBitAnd = true;
229
+ needPreamble("BitAnd");
210
230
  return `BitAnd(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
211
231
  }
212
232
  // int * real coercion: wrap int side with "as real"
@@ -228,7 +248,7 @@ function emitExpr(e) {
228
248
  case "app": {
229
249
  const args = e.args.map(emitExpr);
230
250
  if (e.fn === "SetToSeq") {
231
- needsSetToSeq = true;
251
+ needPreamble("SetToSeq");
232
252
  return `SetToSeq(${args.join(", ")})`;
233
253
  }
234
254
  if (e.fn === "BigInt" || e.fn === "Number")
@@ -237,19 +257,19 @@ function emitExpr(e) {
237
257
  if (e.fn === "SetLiteral")
238
258
  return `{${args.join(", ")}}`;
239
259
  if (e.fn === "JSFloorDiv")
240
- needsJSFloorDiv = true;
260
+ needPreamble("JSFloorDiv");
241
261
  if (e.fn === "CeilReal")
242
- needsCeilReal = true;
262
+ needPreamble("CeilReal");
243
263
  if (e.fn === "FloorReal")
244
- needsFloorReal = true;
264
+ needPreamble("FloorReal");
245
265
  if (e.fn === "NatToString")
246
- needsNatToString = true;
266
+ needPreamble("NatToString");
247
267
  if (e.fn === "MathAbs")
248
- needsMathAbs = true;
268
+ needPreamble("MathAbs");
249
269
  if (e.fn === "MathMin")
250
- needsMathMin = true;
270
+ needPreamble("MathMin");
251
271
  if (e.fn === "MathMax")
252
- needsMathMax = true;
272
+ needPreamble("MathMax");
253
273
  return `${escapeName(e.fn)}(${args.join(", ")})`;
254
274
  }
255
275
  case "field": {
@@ -295,7 +315,7 @@ function emitExpr(e) {
295
315
  if (f)
296
316
  return emitExpr(f.value);
297
317
  if (sf.type.kind === "optional") {
298
- needsOptionType = true;
318
+ needPreamble("OptionType");
299
319
  return "None";
300
320
  }
301
321
  return `/* missing: ${sf.name} */`;
@@ -311,34 +331,12 @@ function emitExpr(e) {
311
331
  case "if":
312
332
  return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
313
333
  case "match": {
314
- const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
334
+ const scrut = emitScrutinee(e.scrutinee);
315
335
  const arms = e.arms.map(a => `case ${translatePattern(a.pattern)} => ${emitExpr(a.body)}`);
316
336
  return `(match ${scrut} { ${arms.join(" ")} })`;
317
337
  }
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
- }
338
+ case "forall": return emitQuantifier(e, "forall");
339
+ case "exists": return emitQuantifier(e, "exists");
342
340
  case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
343
341
  case "havoc": return "*";
344
342
  }
@@ -350,7 +348,7 @@ function emitPureExpr(e, indent) {
350
348
  case "if":
351
349
  return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
352
350
  case "match": {
353
- const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
351
+ const scrut = emitScrutinee(e.scrutinee);
354
352
  const lines = [`${pad}match ${scrut} {`];
355
353
  for (const arm of e.arms) {
356
354
  lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
@@ -415,7 +413,7 @@ function emitStmt(s, indent) {
415
413
  return out;
416
414
  }
417
415
  case "match": {
418
- const scrut = typeof s.scrutinee === "string" ? escapeName(s.scrutinee) : emitExpr(s.scrutinee);
416
+ const scrut = emitScrutinee(s.scrutinee);
419
417
  const lines = [`${pad}match ${scrut} {`];
420
418
  for (const arm of s.arms) {
421
419
  lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
@@ -480,8 +478,10 @@ function emitDecl(d) {
480
478
  lines.push(`}`);
481
479
  // Companion lemma for ensures (proof target for LLM)
482
480
  if (d.ensures.length > 0) {
481
+ // Strip constraints like (==) from type params — ghost lemmas don't need them
482
+ const lemmaTP = d.typeParams.length > 0 ? `<${d.typeParams.map(t => t.replace(/\(.*\)/, '')).join(", ")}>` : "";
483
483
  lines.push("");
484
- lines.push(`lemma ${d.name}_ensures${tp}(${paramList(d.params)})`);
484
+ lines.push(`lemma ${d.name}_ensures${lemmaTP}(${paramList(d.params)})`);
485
485
  for (const r of d.requires)
486
486
  lines.push(` requires ${emitExpr(r)}`);
487
487
  for (const e of d.ensures)
@@ -534,21 +534,9 @@ function emitDecl(d) {
534
534
  }
535
535
  // ── File emission ───────────────────────────────────────────
536
536
  // ── 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;
537
+ /** Preamble tracking — emitters add keys via `needPreamble(key)`, emitDafnyFile emits them. */
538
+ const _neededPreambles = new Set();
539
+ function needPreamble(key) { _neededPreambles.add(key); }
552
540
  const POW2 = `function Pow2(n: int): int
553
541
  requires n >= 0
554
542
  decreases n
@@ -581,6 +569,25 @@ const CEIL_REAL = `function CeilReal(x: real): int
581
569
  if x == (x.Floor as real) then x.Floor
582
570
  else x.Floor + 1
583
571
  }`;
572
+ const SEQ_INDEX_OF = `function SeqIndexOf<T(==)>(s: seq<T>, x: T): int
573
+ ensures -1 <= SeqIndexOf(s, x) < |s|
574
+ ensures SeqIndexOf(s, x) >= 0 ==> s[SeqIndexOf(s, x)] == x
575
+ ensures SeqIndexOf(s, x) == -1 ==> x !in s
576
+ {
577
+ SeqIndexOfFrom(s, x, 0)
578
+ }
579
+
580
+ function SeqIndexOfFrom<T(==)>(s: seq<T>, x: T, from: nat): int
581
+ requires from <= |s|
582
+ ensures -1 <= SeqIndexOfFrom(s, x, from) < |s|
583
+ ensures SeqIndexOfFrom(s, x, from) >= 0 ==> s[SeqIndexOfFrom(s, x, from)] == x
584
+ ensures SeqIndexOfFrom(s, x, from) == -1 ==> forall i :: from <= i < |s| ==> s[i] != x
585
+ decreases |s| - from
586
+ {
587
+ if from == |s| then -1
588
+ else if s[from] == x then from as int
589
+ else SeqIndexOfFrom(s, x, from + 1)
590
+ }`;
584
591
  const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
585
592
  {
586
593
  StringIndexOfFrom(s, sub, 0)
@@ -646,6 +653,42 @@ const NAT_TO_STRING = `function NatToString(n: nat): string
646
653
  else NatToString(n / 10) + [digit]
647
654
  }`;
648
655
  const MATH_ABS = `function MathAbs(x: int): nat { if x >= 0 then x else -x }`;
656
+ const SET_TO_SEQ = `method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
657
+ ensures forall x :: x in s <==> x in res
658
+ ensures |res| == |s|
659
+ {
660
+ var remaining := s;
661
+ res := [];
662
+ while remaining != {}
663
+ invariant remaining <= s
664
+ invariant forall x :: x in res <==> (x in s && x !in remaining)
665
+ invariant |res| + |remaining| == |s|
666
+ decreases remaining
667
+ {
668
+ var x :| x in remaining;
669
+ res := res + [x];
670
+ remaining := remaining - {x};
671
+ }
672
+ }`;
673
+ /** Preamble code keyed by name. Emitted in this order when needed. */
674
+ const PREAMBLE_CODE = [
675
+ ["OptionType", "datatype Option<T> = None | Some(value: T)"],
676
+ ["SetToSeq", SET_TO_SEQ],
677
+ ["Pow2", POW2],
678
+ ["BitAnd", BIT_AND],
679
+ ["JSFloorDiv", JS_FLOOR_DIV],
680
+ ["CeilReal", CEIL_REAL],
681
+ ["FloorReal", FLOOR_REAL],
682
+ ["SeqIndexOf", SEQ_INDEX_OF],
683
+ ["StringIndexOf", STRING_INDEX_OF],
684
+ ["StringTrim", STRING_TRIM],
685
+ ["StringToLower", STRING_TO_LOWER],
686
+ ["StringToUpper", STRING_TO_UPPER],
687
+ ["NatToString", NAT_TO_STRING],
688
+ ["MathAbs", MATH_ABS],
689
+ ["MathMin", MATH_MIN],
690
+ ["MathMax", MATH_MAX],
691
+ ];
649
692
  // ── Constructor and record helpers ───────────────────────────
650
693
  let _recordCtors = new Map();
651
694
  let _structureDecls = new Map();
@@ -714,26 +757,9 @@ function translatePattern(pattern) {
714
757
  const fieldNames = fields.split(/\s+/).map(escapeName);
715
758
  return `${ctorName}(${fieldNames.join(", ")})`;
716
759
  }
717
- const PREAMBLES = {
718
- StringIndexOf: STRING_INDEX_OF,
719
- };
720
760
  export function emitDafnyFile(file, tsFileName) {
721
761
  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;
762
+ _neededPreambles.clear();
737
763
  // Collect pure def names so we can skip their method wrappers
738
764
  const pureDefs = new Set();
739
765
  for (const d of file.decls) {
@@ -770,81 +796,11 @@ export function emitDafnyFile(file, tsFileName) {
770
796
  const lines = [];
771
797
  if (tsFileName)
772
798
  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);
799
+ for (const [key, code] of PREAMBLE_CODE) {
800
+ if (_neededPreambles.has(key)) {
801
+ lines.push("");
802
+ lines.push(code);
803
+ }
848
804
  }
849
805
  lines.push(...declLines);
850
806
  return lines.join("\n") + "\n";
@@ -196,6 +196,7 @@ function extractExpr(node) {
196
196
  if (Node.isObjectLiteralExpression(node)) {
197
197
  let spread = null;
198
198
  const fields = [];
199
+ const computedFields = [];
199
200
  for (const prop of node.getProperties()) {
200
201
  if (Node.isSpreadAssignment(prop)) {
201
202
  spread = extractExpr(prop.getExpression());
@@ -205,10 +206,26 @@ function extractExpr(node) {
205
206
  fields.push({ name, value: { kind: "var", name } });
206
207
  }
207
208
  else if (Node.isPropertyAssignment(prop)) {
209
+ const nameNode = prop.getNameNode();
208
210
  const init = prop.getInitializer();
209
- if (init)
211
+ if (init && Node.isComputedPropertyName(nameNode)) {
212
+ computedFields.push({ key: extractExpr(nameNode.getExpression()), value: extractExpr(init) });
213
+ }
214
+ else if (init) {
210
215
  fields.push({ name: prop.getName(), value: extractExpr(init) });
216
+ }
217
+ }
218
+ }
219
+ // Desugar computed keys: { ...base, [k]: v } → base.set(k, v)
220
+ // No spread: { [k]: v } → {}.set(k, v) (empty map base)
221
+ if (computedFields.length > 0) {
222
+ let result = spread ?? { kind: "record", spread: null, fields: [] };
223
+ for (const cf of computedFields) {
224
+ result = { kind: "call",
225
+ fn: { kind: "field", obj: result, field: "set" },
226
+ args: [cf.key, cf.value] };
211
227
  }
228
+ return result;
212
229
  }
213
230
  return { kind: "record", spread, fields };
214
231
  }
@@ -234,8 +251,9 @@ function extractExpr(node) {
234
251
  if (name === "Map" && args && args.length === 1) {
235
252
  const argType = args[0].getType();
236
253
  const argSymbol = argType.getSymbol()?.getName() ?? argType.getAliasSymbol()?.getName();
237
- if (argSymbol === "Map") {
238
- // new Map(existingMap) identity (Dafny maps are value types)
254
+ const argTypeText = _eraseGenerics(typeToString(argType));
255
+ if (argSymbol === "Map" || argTypeText.startsWith("Record<")) {
256
+ // new Map(existingMap) or new Map(record) — identity (Dafny maps are value types)
239
257
  return extractExpr(args[0]);
240
258
  }
241
259
  // new Map(entries) — map-from-array constructor
@@ -257,6 +275,17 @@ function extractExpr(node) {
257
275
  if (Node.isAsExpression(node)) {
258
276
  return extractExpr(node.getExpression());
259
277
  }
278
+ // delete obj[key] → map delete expression
279
+ if (Node.isDeleteExpression(node)) {
280
+ const expr = node.getExpression();
281
+ if (Node.isElementAccessExpression(expr)) {
282
+ return {
283
+ kind: "call",
284
+ fn: { kind: "field", obj: extractExpr(expr.getExpression()), field: "delete" },
285
+ args: [extractExpr(expr.getArgumentExpression())],
286
+ };
287
+ }
288
+ }
260
289
  // null → undefined (both map to None in backends)
261
290
  if (Node.isNullLiteral(node)) {
262
291
  return { kind: "var", name: "undefined" };
@@ -511,6 +540,38 @@ function extractStmts(stmts) {
511
540
  }
512
541
  continue;
513
542
  }
543
+ // Destructuring rest: const { [k]: _, ...rest } = map → let rest = map.delete(k)
544
+ if (!isHavoc && Node.isObjectBindingPattern(nameNode)) {
545
+ const elements = nameNode.getElements();
546
+ const restEl = elements.find(el => el.getDotDotDotToken());
547
+ const computedEls = elements.filter(el => {
548
+ const pn = el.getPropertyNameNode();
549
+ return pn && Node.isComputedPropertyName(pn);
550
+ });
551
+ if (restEl && computedEls.length > 0) {
552
+ const initializer = d.getInitializer();
553
+ if (initializer) {
554
+ let deleteInit = extractExpr(initializer);
555
+ for (const cel of computedEls) {
556
+ const pn = cel.getPropertyNameNode();
557
+ const keyExpr = extractExpr(pn.getExpression());
558
+ deleteInit = { kind: "call",
559
+ fn: { kind: "field", obj: deleteInit, field: "delete" },
560
+ args: [keyExpr] };
561
+ }
562
+ const declType = d.getType();
563
+ result.push({
564
+ kind: "let",
565
+ name: restEl.getName(),
566
+ mutable: s.getDeclarationKind() === "let",
567
+ tsType: _eraseGenerics(typeToString(declType)),
568
+ init: deleteInit,
569
+ line,
570
+ });
571
+ continue;
572
+ }
573
+ }
574
+ }
514
575
  const declType = d.getType();
515
576
  let init;
516
577
  if (isHavoc && !havocKey) {
@@ -558,7 +619,9 @@ function extractStmts(stmts) {
558
619
  const nameNode = decl?.getNameNode();
559
620
  if (nameNode && Node.isArrayBindingPattern(nameNode)) {
560
621
  for (const elem of nameNode.getElements()) {
561
- if (Node.isBindingElement(elem))
622
+ if (Node.isOmittedExpression(elem))
623
+ names.push("_");
624
+ else if (Node.isBindingElement(elem))
562
625
  names.push(elem.getNameNode().getText());
563
626
  }
564
627
  }
@@ -583,6 +646,27 @@ function extractStmts(stmts) {
583
646
  });
584
647
  continue;
585
648
  }
649
+ // for...in: for (const k in obj) → treat as forof with single key name
650
+ if (Node.isForInStatement(s)) {
651
+ const init = s.getInitializer();
652
+ let name = "_";
653
+ if (Node.isVariableDeclarationList(init)) {
654
+ name = init.getDeclarations()[0]?.getName() ?? "_";
655
+ }
656
+ const bodyNode = s.getStatement();
657
+ const bodyStmts = Node.isBlock(bodyNode) ? bodyNode.getStatements() : [bodyNode];
658
+ const annots = collectAnnotations(s, bodyStmts);
659
+ result.push({
660
+ kind: "forof",
661
+ names: [name],
662
+ iterable: extractExpr(s.getExpression()),
663
+ invariants: annots.filter(a => a.kind === "invariant").map(a => a.expr),
664
+ doneWith: annots.find(a => a.kind === "done_with")?.expr ?? null,
665
+ body: extractStmts(bodyStmts),
666
+ line,
667
+ });
668
+ continue;
669
+ }
586
670
  if (Node.isIfStatement(s)) {
587
671
  const thenNode = s.getThenStatement();
588
672
  const elseNode = s.getElseStatement();
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
  }