lemmascript 0.5.0 → 0.5.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.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
5
5
  "type": "module",
6
6
  "engines": {
@@ -35,6 +35,15 @@ const DAFNY_KEYWORDS = new Set([
35
35
  "datatype", "type", "const", "ghost", "static",
36
36
  "reads", "modifies", "assert", "assume", "print",
37
37
  "by", "calc", "reveal",
38
+ // Further reserved words (validated against the Dafny parser) that are also
39
+ // legal TS identifiers. `this` stays excluded above — class methods emit it
40
+ // directly. `then`/`else` already covered.
41
+ "bool", "char", "int", "nat", "real", "string", "object", "array",
42
+ "as", "is", "label", "modify", "expect", "yield", "yields", "returns",
43
+ "unchanged", "witness", "constructor", "iterator", "abstract", "extends",
44
+ "refines", "opened", "provides", "reveals", "include", "newtype",
45
+ "codatatype", "nameonly", "twostate", "opaque", "replaceable", "colemma",
46
+ "copredicate", "inductive",
38
47
  ]);
39
48
  function escapeName(name) {
40
49
  // \result is carried through the IR as the var name "\\result"; render it
@@ -1502,17 +1502,39 @@ function extractStmts(stmts) {
1502
1502
  const switchExpr = exprAst.kind === "field" ? exprAst.obj : exprAst;
1503
1503
  const cases = [];
1504
1504
  let defaultBody = [];
1505
+ // Two JS-`switch` faithfulness concerns the Dafny `match` doesn't share:
1506
+ // (1) Fall-through: stacked `case A: case B: body` is several clauses
1507
+ // where the leading ones have no statements; those labels share the
1508
+ // next clause's body (we duplicate it per label).
1509
+ // (2) `break` is the switch exit, not a loop break. We extract the full
1510
+ // body (extractStmts flattens `{ }` blocks but keeps loop bodies
1511
+ // nested) and strip the *top-level* breaks — so a `break` written
1512
+ // inside a `{ }` case block is stripped, while a `break` inside a
1513
+ // nested loop stays put.
1514
+ const stripExitBreaks = (b) => b.filter(st => st.kind !== "break");
1515
+ let fallthrough = [];
1505
1516
  for (const clause of s.getClauses()) {
1506
1517
  if (Node.isCaseClause(clause)) {
1507
1518
  const label = clause.getExpression().getText().replace(/^["']|["']$/g, "");
1508
- const bodyStmts = clause.getStatements().filter(st => !Node.isBreakStatement(st));
1509
- cases.push({ label, body: extractStmts(bodyStmts) });
1519
+ if (clause.getStatements().length === 0) {
1520
+ fallthrough.push(label);
1521
+ continue;
1522
+ }
1523
+ const body = stripExitBreaks(extractStmts(clause.getStatements()));
1524
+ for (const l of fallthrough)
1525
+ cases.push({ label: l, body });
1526
+ cases.push({ label, body });
1527
+ fallthrough = [];
1510
1528
  }
1511
1529
  else {
1512
- const bodyStmts = clause.getStatements().filter(st => !Node.isBreakStatement(st));
1513
- defaultBody = extractStmts(bodyStmts);
1530
+ defaultBody = stripExitBreaks(extractStmts(clause.getStatements()));
1531
+ for (const l of fallthrough)
1532
+ cases.push({ label: l, body: defaultBody });
1533
+ fallthrough = [];
1514
1534
  }
1515
1535
  }
1536
+ for (const l of fallthrough)
1537
+ cases.push({ label: l, body: [] });
1516
1538
  result.push({ kind: "switch", expr: switchExpr, discriminant, cases, defaultBody, line });
1517
1539
  continue;
1518
1540
  }
@@ -1735,7 +1757,19 @@ export function extractModule(sourceFile) {
1735
1757
  }
1736
1758
  const aliasMatch = body.match(/^(\w+)\s*=\s*(.+)$/);
1737
1759
  if (aliasMatch) {
1738
- typeDecls.push({ name: aliasMatch[1], kind: "alias", aliasOf: aliasMatch[2].trim() });
1760
+ const rhs = aliasMatch[2].trim();
1761
+ // A string-literal union (`= "a" | "b" | …`) becomes an enum datatype —
1762
+ // the same shape a real string-union alias resolves to. Dafny has no
1763
+ // string-literal type, so a plain alias (`type X = "a" | "b"`) would be
1764
+ // invalid. Other RHS forms (`Rule[]`, `number`, `A | B`) fall through.
1765
+ const parts = rhs.split("|").map(s => s.trim());
1766
+ const lits = parts.map(p => p.match(/^["'](.+)["']$/));
1767
+ if (parts.length >= 2 && lits.every(m => m !== null)) {
1768
+ typeDecls.push({ name: aliasMatch[1], kind: "string-union", values: lits.map(m => m[1]) });
1769
+ }
1770
+ else {
1771
+ typeDecls.push({ name: aliasMatch[1], kind: "alias", aliasOf: rhs });
1772
+ }
1739
1773
  }
1740
1774
  }
1741
1775
  for (const range of sourceFile.getLeadingCommentRanges()) {
@@ -290,6 +290,41 @@ function expandAlias(ty, typeDecls, seen = new Set()) {
290
290
  function getDiscriminant(ctx, typeName) {
291
291
  return findDecl(ctx, typeName)?.discriminant;
292
292
  }
293
+ // ── Equality hazard: structural in the proof vs reference at runtime ─────────
294
+ // `===`/`!==` is modeled as Dafny structural equality, but the SAME TypeScript
295
+ // runs `===` as JS *reference* equality on objects/arrays. The two agree only
296
+ // when the operand is a primitive at runtime: number / string / bool, or a
297
+ // string-union enum (which runs as a plain string). Records, discriminated
298
+ // unions, arrays, maps, sets, and unresolved generics are reference-compared at
299
+ // runtime, so a structural proof over them is unsound. Returns true for those.
300
+ function refEqHazard(ty, typeDecls) {
301
+ if (ty.kind === "array" || ty.kind === "map" || ty.kind === "set")
302
+ return true;
303
+ if (ty.kind === "user") {
304
+ let decl = typeDecls.find(d => d.name === ty.name);
305
+ if (!decl && ty.name.includes(".")) {
306
+ const tail = ty.name.slice(ty.name.lastIndexOf(".") + 1);
307
+ decl = typeDecls.find(d => d.name === tail);
308
+ }
309
+ if (!decl)
310
+ return true; // generic type parameter / unknown → assume reference
311
+ if (decl.kind === "string-union")
312
+ return false; // runs as a JS string → `===` is structural
313
+ if (decl.kind === "alias")
314
+ return decl.aliasOfTy ? refEqHazard(decl.aliasOfTy, typeDecls) : false;
315
+ return true; // record / discriminated-union → reference at runtime
316
+ }
317
+ return false; // primitives, optional, unknown, fn, void
318
+ }
319
+ const _warnedRefEq = new Set();
320
+ function warnRefEq(op, l, r) {
321
+ const label = (t) => t.kind === "user" ? t.name : t.kind;
322
+ const msg = `'${op}' compares non-primitive operands (${label(l)} ${op} ${label(r)}): structural equality in the proof, but reference equality when this TypeScript runs. Sound only if operands are primitives or a canonical (string/number) encoding; otherwise compare via an explicit structural equals.`;
323
+ if (_warnedRefEq.has(msg))
324
+ return;
325
+ _warnedRefEq.add(msg);
326
+ console.error(`WARNING: ${msg}`);
327
+ }
293
328
  /** A type ts-morph handed us that LemmaScript hasn't modeled: contains
294
329
  * `unknown` (TS `any`), or a `user` type whose name isn't a known declaration
295
330
  * (an opaque expanded union like `"AssistantMsg | ToolMsg"` that ts-morph
@@ -606,6 +641,11 @@ function resolveExpr(e, ctx) {
606
641
  if (e.op === "===" || e.op === "!==") {
607
642
  left = coerceStr(left, right.ty);
608
643
  right = coerceStr(right, left.ty);
644
+ // Spec (`//@`) comparisons are proof-only, so they can't diverge at
645
+ // runtime; only warn on executable code.
646
+ if (!ctx.inSpec && refEqHazard(left.ty, ctx.typeDecls) && refEqHazard(right.ty, ctx.typeDecls)) {
647
+ warnRefEq(e.op, left.ty, right.ty);
648
+ }
609
649
  }
610
650
  let ty = { kind: "unknown" };
611
651
  if (["===", "!==", ">=", "<=", ">", "<", "in"].includes(e.op))
@@ -1388,6 +1428,7 @@ function precomputeFieldTypesInner(typeDecls) {
1388
1428
  }
1389
1429
  }
1390
1430
  export function resolveModule(raw) {
1431
+ _warnedRefEq.clear();
1391
1432
  precomputeFieldTypes(raw.typeDecls);
1392
1433
  const pureFns = computePureFns(raw.functions);
1393
1434
  // Pre-compute function parameter and return types