lemmascript 0.2.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.
package/README.md CHANGED
@@ -16,8 +16,9 @@ See the external case studies:
16
16
  - **[colorwheel-lemmascript](https://github.com/midspiral/colorwheel-lemmascript/)** — verified color palette generator with mood + harmony constraints. 31 Lean proofs + 18 behavioral properties, 115 Dafny lemmas (invariant preservation, commutativity, NoOp completeness).
17
17
  - **[clear-split-lemmascript](https://github.com/midspiral/clear-split-lemmascript/)** — greenfield verified expense splitting web app. Conservation theorem, invariant preservation, delta laws — all proven in both Lean (no sorry) and Dafny (56 lemmas).
18
18
  - **[node-casbin-lemmascript](https://github.com/midspiral/node-casbin-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [node-casbin](https://github.com/casbin/node-casbin). 5 functions verified, 217 existing tests pass. End-to-end correctness and order independence for all 4 effect modes in both Lean and Dafny (39 lemmas).
19
- - **[hono-lemmascript](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [hono](https://github.com/honojs/hono)'s security middleware. Two CVEs verified: IP restriction bypass ([CVE-2026-39409](https://github.com/honojs/hono/security/advisories/GHSA-3mpf-rcc7-5347)) and cookie name bypass ([CVE-2026-39410](https://github.com/honojs/hono/security/advisories/GHSA-r5rp-j6wh-rvv4)) — 51 Dafny lemmas. Cookie verification done in-place. Dafny only.
20
- - **[charmchat](https://github.com/CHARM-BDF/charmchat/tree/lemma)** — in-progress verification of some sub-components, in Dafny.
19
+ - **[hono-lemmascript](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [hono](https://github.com/honojs/hono)'s security middleware. Two CVEs verified: IP restriction bypass ([CVE-2026-39409](https://github.com/honojs/hono/security/advisories/GHSA-3mpf-rcc7-5347)) and cookie name bypass ([CVE-2026-39410](https://github.com/honojs/hono/security/advisories/GHSA-r5rp-j6wh-rvv4)) — 51 Dafny lemmas. [Cookie verification done **in-place**](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/src/utils/cookie.ts#L79). Dafny only.
20
+ - **[charmchat](https://github.com/CHARM-BDF/charmchat/tree/lemma)** — brownfield verification of an AI agent orchestration backend. `isEmptyResult` (string emptiness predicate, 8 postconditions, <1s) and `topologicalSort` (Kahn's algorithm — memory safety, output bounds, completeness via acyclicity ranking witness, termination; 5 helper lemmas, 28 loop invariants). Dafny only.
21
+ - **[xyflow-lemmascript](https://github.com/midspiral/xyflow-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [xyflow](https://github.com/xyflow/xyflow)'s core edge and geometry utilities. 9 functions verified: `addEdge` (dedup — never loses edges, adds at most one), `reconnectEdge` (replace — bounded length), `connectionExists`, `getEdgeCenter` (midpoint correctness), `clamp` (bounds), `rectToBox`/`boxToRect` (field arithmetic), `getBoundsOfBoxes` (enclosure), `getOverlappingArea` (non-negative), `areSetsEqual` (subset + same size). 14 Dafny proof obligations. Dafny only.
21
22
 
22
23
  ## Setup
23
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemmascript",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,7 +35,7 @@ export function dafnyVerify(dfyPath, dir, timeLimit) {
35
35
  console.log("Running dafny verify...");
36
36
  try {
37
37
  const content = readFileSync(dfyPath, "utf-8");
38
- const stdLibFlag = content.includes("import Std.") ? " --standard-libraries" : "";
38
+ const stdLibFlag = content.includes("Std.") ? " --standard-libraries" : "";
39
39
  const timeLimitFlag = timeLimit ? ` --verification-time-limit ${timeLimit}` : "";
40
40
  execSync(`dafny verify${stdLibFlag}${timeLimitFlag} "${dfyPath}"`, { cwd: dir, stdio: "inherit" });
41
41
  return true;
@@ -51,12 +51,13 @@ function paramList(params) {
51
51
  const OP_MAP = {
52
52
  "=": "==", "≠": "!=", "≥": ">=", "≤": "<=",
53
53
  "∧": "&&", "∨": "||", "¬": "!",
54
+ "arrayConcat": "+",
54
55
  };
55
56
  function mapOp(op) { return OP_MAP[op] ?? op; }
56
57
  // ── Expression emission ─────────────────────────────────────
57
58
  function emitExpr(e) {
58
59
  switch (e.kind) {
59
- case "var": return escapeName(e.name);
60
+ case "var": return e.name === "undefined" ? "None" : escapeName(e.name);
60
61
  case "num": return `${e.value}`;
61
62
  case "bool": return e.value ? "true" : "false";
62
63
  case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
@@ -79,20 +80,18 @@ function emitExpr(e) {
79
80
  return `(${args[0]} in ${obj})`;
80
81
  if (e.method === "push")
81
82
  return `(${obj} + [${args[0]}])`;
82
- if (e.method === "slice")
83
+ if (e.method === "concat")
84
+ return `(${obj} + [${args[0]}])`;
85
+ if (e.method === "slice" && args.length === 1)
83
86
  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
- }
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]})`;
96
95
  if (e.method === "some" && e.args[0].kind === "lambda" &&
97
96
  e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
98
97
  const lam = e.args[0];
@@ -143,6 +142,8 @@ function emitExpr(e) {
143
142
  return `${obj}[${args[0]} := ${args[1]}]`;
144
143
  if (e.method === "has")
145
144
  return `(${args[0]} in ${obj})`;
145
+ if (e.method === "delete")
146
+ return `(map k | k in ${obj} && k != ${args[0]} :: ${obj}[k])`;
146
147
  }
147
148
  // Set methods
148
149
  if (ty === "set") {
@@ -232,13 +233,24 @@ function emitExpr(e) {
232
233
  }
233
234
  if (e.fn === "BigInt" || e.fn === "Number")
234
235
  return args[0]; // identity: both map to int
236
+ // Set literal: {a, b, c}
237
+ if (e.fn === "SetLiteral")
238
+ return `{${args.join(", ")}}`;
235
239
  if (e.fn === "JSFloorDiv")
236
240
  needsJSFloorDiv = true;
237
241
  if (e.fn === "CeilReal")
238
242
  needsCeilReal = true;
239
243
  if (e.fn === "FloorReal")
240
244
  needsFloorReal = true;
241
- return `${e.fn}(${args.join(", ")})`;
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(", ")})`;
242
254
  }
243
255
  case "field": {
244
256
  const obj = emitExpr(e.obj);
@@ -260,10 +272,40 @@ function emitExpr(e) {
260
272
  const updates = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
261
273
  return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
262
274
  }
263
- const ctorName = e.fields.length > 0 ? _recordCtors.get(e.fields[0].name) : undefined;
264
- const vals = e.fields.map(f => emitExpr(f.value));
265
- if (ctorName)
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));
266
306
  return `${ctorName}(${vals.join(", ")})`;
307
+ }
308
+ const vals = e.fields.map(f => emitExpr(f.value));
267
309
  return `(${vals.join(", ")})`;
268
310
  }
269
311
  case "if":
@@ -331,7 +373,13 @@ function emitStmt(s, indent) {
331
373
  const pad = " ".repeat(indent);
332
374
  switch (s.kind) {
333
375
  case "let":
334
- if (s.value.kind === "havoc")
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))
335
383
  return `${pad}var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
336
384
  return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
337
385
  case "assign":
@@ -408,18 +456,23 @@ function emitStmt(s, indent) {
408
456
  function emitDecl(d) {
409
457
  switch (d.kind) {
410
458
  case "inductive": {
459
+ const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
411
460
  const ctors = d.constructors.map(c => {
412
461
  if (c.fields.length === 0)
413
462
  return escapeName(c.name);
414
463
  return `${escapeName(c.name)}(${paramList(c.fields)})`;
415
464
  });
416
- return `datatype ${d.name} = ${ctors.join(" | ")}`;
465
+ return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
417
466
  }
418
467
  case "structure": {
419
468
  return `datatype ${d.name} = ${d.name}(${paramList(d.fields)})`;
420
469
  }
470
+ case "type-alias": {
471
+ return `type ${d.name} = ${tyToDafny(d.target)}`;
472
+ }
421
473
  case "def": {
422
- const lines = [`function ${d.name}(${paramList(d.params)}): ${tyToDafny(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)}`];
423
476
  for (const r of d.requires)
424
477
  lines.push(` requires ${emitExpr(r)}`);
425
478
  lines.push(`{`);
@@ -428,7 +481,7 @@ function emitDecl(d) {
428
481
  // Companion lemma for ensures (proof target for LLM)
429
482
  if (d.ensures.length > 0) {
430
483
  lines.push("");
431
- lines.push(`lemma ${d.name}_ensures(${paramList(d.params)})`);
484
+ lines.push(`lemma ${d.name}_ensures${tp}(${paramList(d.params)})`);
432
485
  for (const r of d.requires)
433
486
  lines.push(` requires ${emitExpr(r)}`);
434
487
  for (const e of d.ensures)
@@ -439,7 +492,8 @@ function emitDecl(d) {
439
492
  return lines.join("\n");
440
493
  }
441
494
  case "method": {
442
- const lines = [`method ${d.name}(${paramList(d.params)}) returns (res: ${tyToDafny(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)})`];
443
497
  for (const r of d.requires)
444
498
  lines.push(` requires ${emitExpr(r)}`);
445
499
  for (const e of d.ensures)
@@ -487,11 +541,14 @@ let needsStringToUpper = false;
487
541
  let needsJSFloorDiv = false;
488
542
  let needsCeilReal = false;
489
543
  let needsFloorReal = false;
490
- let needsStdCollections = false;
491
544
  let needsOptionType = false;
492
545
  let needsSetToSeq = false;
493
546
  let needsBitAnd = false;
494
547
  let needsPow2 = false;
548
+ let needsNatToString = false;
549
+ let needsMathAbs = false;
550
+ let needsMathMin = false;
551
+ let needsMathMax = false;
495
552
  const POW2 = `function Pow2(n: int): int
496
553
  requires n >= 0
497
554
  decreases n
@@ -579,25 +636,64 @@ const STRING_TO_UPPER = `function StringToUpper(s: string): string
579
636
  var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
580
637
  [upper] + StringToUpper(s[1..])
581
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 }`;
582
649
  // ── Constructor and record helpers ───────────────────────────
583
650
  let _recordCtors = new Map();
651
+ let _structureDecls = new Map();
652
+ let _declaredTypes = new Set();
584
653
  function buildRecordCtorMap(decls) {
585
654
  _recordCtors = new Map();
586
- for (const d of decls) {
587
- if (d.kind === "structure" && d.fields.length > 0)
588
- _recordCtors.set(d.fields[0].name, d.name);
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);
589
670
  if (d.kind === "namespace")
590
- for (const inner of d.decls) {
591
- if (inner.kind === "structure" && inner.fields.length > 0)
592
- _recordCtors.set(inner.fields[0].name, inner.name);
593
- }
671
+ for (const inner of d.decls)
672
+ collectDecl(inner);
594
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;
595
690
  }
596
691
  function qualifyCtor(name, type) {
597
692
  const rawName = name.replace(/^\./, "");
693
+ const mapped = CTOR_MAP[rawName] ?? escapeName(rawName);
598
694
  if (type)
599
- return `${type}.${escapeName(rawName)}`;
600
- return escapeName(rawName);
695
+ return `${type}.${mapped}`;
696
+ return mapped;
601
697
  }
602
698
  /** Translate a Lean match pattern to Dafny syntax.
603
699
  * ".ctorName field1 field2" → "ctorName(field1, field2)"
@@ -630,11 +726,14 @@ export function emitDafnyFile(file, tsFileName) {
630
726
  needsJSFloorDiv = false;
631
727
  needsCeilReal = false;
632
728
  needsFloorReal = false;
633
- needsStdCollections = false;
634
729
  needsOptionType = false;
635
730
  needsSetToSeq = false;
636
731
  needsBitAnd = false;
637
732
  needsPow2 = false;
733
+ needsNatToString = false;
734
+ needsMathAbs = false;
735
+ needsMathMin = false;
736
+ needsMathMax = false;
638
737
  // Collect pure def names so we can skip their method wrappers
639
738
  const pureDefs = new Set();
640
739
  for (const d of file.decls) {
@@ -671,8 +770,6 @@ export function emitDafnyFile(file, tsFileName) {
671
770
  const lines = [];
672
771
  if (tsFileName)
673
772
  lines.push(`// Generated by lsc from ${tsFileName}`);
674
- if (needsStdCollections)
675
- lines.push("import Std.Collections.Seq");
676
773
  if (needsOptionType) {
677
774
  lines.push("");
678
775
  lines.push("datatype Option<T> = None | Some(value: T)");
@@ -733,6 +830,22 @@ export function emitDafnyFile(file, tsFileName) {
733
830
  lines.push("");
734
831
  lines.push(STRING_TO_UPPER);
735
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
+ }
736
849
  lines.push(...declLines);
737
850
  return lines.join("\n") + "\n";
738
851
  }