lemmascript 0.3.1 → 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.1",
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": {
@@ -94,6 +94,10 @@ function emitExpr(e) {
94
94
  return `${obj}[${args[0]} := ${args[1]}]`;
95
95
  if (e.method === "includes")
96
96
  return `(${args[0]} in ${obj})`;
97
+ if (e.method === "indexOf") {
98
+ needPreamble("SeqIndexOf");
99
+ return `SeqIndexOf(${obj}, ${args[0]})`;
100
+ }
97
101
  if (e.method === "push")
98
102
  return `(${obj} + [${args[0]}])`;
99
103
  if (e.method === "concat")
@@ -565,6 +569,25 @@ const CEIL_REAL = `function CeilReal(x: real): int
565
569
  if x == (x.Floor as real) then x.Floor
566
570
  else x.Floor + 1
567
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
+ }`;
568
591
  const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
569
592
  {
570
593
  StringIndexOfFrom(s, sub, 0)
@@ -656,6 +679,7 @@ const PREAMBLE_CODE = [
656
679
  ["JSFloorDiv", JS_FLOOR_DIV],
657
680
  ["CeilReal", CEIL_REAL],
658
681
  ["FloorReal", FLOOR_REAL],
682
+ ["SeqIndexOf", SEQ_INDEX_OF],
659
683
  ["StringIndexOf", STRING_INDEX_OF],
660
684
  ["StringTrim", STRING_TRIM],
661
685
  ["StringToLower", STRING_TO_LOWER],
@@ -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) {
@@ -585,6 +646,27 @@ function extractStmts(stmts) {
585
646
  });
586
647
  continue;
587
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
+ }
588
670
  if (Node.isIfStatement(s)) {
589
671
  const thenNode = s.getThenStatement();
590
672
  const elseNode = s.getElseStatement();
@@ -293,6 +293,8 @@ function inferMethodReturnTy(fn, args, ctx) {
293
293
  else if (objTy.kind === "array") {
294
294
  if (fn.field === "includes")
295
295
  return { kind: "bool" };
296
+ if (fn.field === "indexOf")
297
+ return { kind: "int" };
296
298
  if (fn.field === "shift")
297
299
  return objTy.elem;
298
300
  if (fn.field === "push" || fn.field === "concat")
@@ -395,7 +397,7 @@ function resolveExpr(e, ctx) {
395
397
  const obj = resolveExpr(e.obj, ctx);
396
398
  const idx = resolveExpr(e.idx, ctx);
397
399
  const idxTy = obj.ty.kind === "array" ? obj.ty.elem
398
- : obj.ty.kind === "map" ? obj.ty.value
400
+ : obj.ty.kind === "map" ? { kind: "optional", inner: obj.ty.value }
399
401
  : { kind: "unknown" };
400
402
  return { kind: "index", obj, idx, ty: idxTy };
401
403
  }
@@ -445,6 +447,10 @@ function resolveExpr(e, ctx) {
445
447
  if (fieldDecl) {
446
448
  const declTy = fieldDecl.type;
447
449
  value = coerceStr(value, declTy);
450
+ // Empty {} for map-typed fields → empty map (arrayLiteral with map type → emptyMap in transform)
451
+ if (value.kind === "record" && value.fields.length === 0 && !value.spread && declTy.kind === "map") {
452
+ value = { kind: "arrayLiteral", elems: [], ty: declTy };
453
+ }
448
454
  // Coerce non-optional to optional: wrap in Some (only when value type is concrete)
449
455
  if (declTy.kind === "optional" && value.ty.kind !== "optional" && value.ty.kind !== "void" && value.ty.kind !== "unknown") {
450
456
  value = wrapSome(value, declTy);
@@ -593,8 +599,10 @@ function resolveBlock(stmts, ctx) {
593
599
  function resolveStmt(s, ctx) {
594
600
  switch (s.kind) {
595
601
  case "let": {
596
- const ty = resolveTsType(s.tsType, ctx.overrides, s.name);
597
- const init = coerceStr(resolveExpr(s.init, ctx), ty);
602
+ const declTy = resolveTsType(s.tsType, ctx.overrides, s.name);
603
+ const init = coerceStr(resolveExpr(s.init, ctx), declTy);
604
+ // Map indexing: TS says T, but access can fail → use Optional<T> from init
605
+ const ty = (declTy.kind !== "optional" && init.ty.kind === "optional") ? init.ty : declTy;
598
606
  // const collections are mutable in value-semantics world (TS mutates in place, Dafny/Lean reassign)
599
607
  const mutable = s.mutable || isRefMutableInTS(ty);
600
608
  return [{ kind: "let", name: s.name, ty, mutable, init }, extend(ctx.env, s.name, ty)];
@@ -350,6 +350,9 @@ function lowerExpr(e, binds) {
350
350
  return { kind: "field", obj: transformExpr(e.obj), field: e.field };
351
351
  case "index": {
352
352
  const idx = transformExpr(e.idx);
353
+ if (e.obj.ty.kind === "map") {
354
+ return { kind: "methodCall", obj: transformExpr(e.obj), objTy: e.obj.ty, method: "get", args: [idx], monadic: false };
355
+ }
353
356
  const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
354
357
  return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
355
358
  }
@@ -637,6 +640,31 @@ function transformStmts(stmts, typeDecls) {
637
640
  const varName = s.names[0];
638
641
  const varTy = s.nameTypes[0] ?? { kind: "unknown" };
639
642
  let iterExpr = transformExpr(s.iterable);
643
+ // Map key-only iteration: for (const k in record) → iterate keys only
644
+ if (s.names.length === 1 && s.iterable.ty.kind === "map") {
645
+ const keyName = s.names[0];
646
+ const keyTy = s.nameTypes[0] ?? s.iterable.ty.key ?? { kind: "unknown" };
647
+ const keysSeqName = `_${keyName}_keys`;
648
+ const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
649
+ result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
650
+ const keysVar = { kind: "var", name: keysSeqName };
651
+ const count = _forofCounters.get(keyName) ?? 0;
652
+ _forofCounters.set(keyName, count + 1);
653
+ const suffix = count === 0 ? "" : `${count + 1}`;
654
+ const idxName = `_${keyName}_idx${suffix}`;
655
+ const idx = { kind: "var", name: idxName };
656
+ const arrSize = { kind: "field", obj: keysVar, field: "size" };
657
+ const bodyStmts = transformStmts(s.body, typeDecls);
658
+ const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
659
+ const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
660
+ result.push({
661
+ kind: "forin", idx: idxName, bound: arrSize,
662
+ invariants: [boundInv, ...s.invariants.map(transformExpr)],
663
+ body: [letKey, ...bodyStmts],
664
+ });
665
+ i++;
666
+ continue;
667
+ }
640
668
  // Map iteration: for (const [k, v] of map) → iterate keys, look up values
641
669
  if (s.names.length >= 2 && s.iterable.ty.kind === "map") {
642
670
  const keyName = s.names[0], valueName = s.names[1];