lemmascript 0.5.12 → 0.5.14

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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Fresh names for toolchain-minted identifiers.
3
+ *
4
+ * Several passes synthesize names — loop counters (`_x_idx`), lift temps
5
+ * (`_t0`), someMatch binders (`_task_val`), quantifier binders. Any such name
6
+ * can collide with a user-written identifier and either capture it (silently
7
+ * changing semantics — the `.delete()` binder bug) or shadow it (a
8
+ * loud duplicate-variable error in the backend).
9
+ *
10
+ * The rule, chosen for zero churn to existing output: a minted name is used
11
+ * verbatim unless the user wrote the same identifier somewhere in the module,
12
+ * in which case a prime (`'`) is appended. TypeScript identifiers cannot
13
+ * contain a prime, while Dafny and Lean both accept them, so one prime always
14
+ * suffices; and priming preserves distinctness among minted names, so the
15
+ * existing counter/suffix schemes keep working unchanged.
16
+ *
17
+ * The module-wide check deliberately over-approximates scope — a colliding
18
+ * name anywhere in the file primes the mint. False positives only affect
19
+ * internal names nobody reads. Names with a user-facing meaning stay exact by
20
+ * different, local means: comprehension binders check the expressions they
21
+ * actually wrap (`usesName` in dafny-emit), and result binders check the
22
+ * signature/body in hand (`methodHeader`). Spec text (`//@` comments) is NOT
23
+ * scanned: specs may legitimately reference minted names (e.g. `_x_idx` loop
24
+ * counters in invariants), so a spec mention is a reference, not a collision
25
+ * — when a source collision does force a prime, spec references are expected
26
+ * to follow it.
27
+ */
28
+ let _userNames = new Set();
29
+ /** Seeded once per module by extract with every Identifier token in the
30
+ * source — params, locals, fields, callees alike. */
31
+ export function setUserNames(names) {
32
+ _userNames = names;
33
+ }
34
+ export function isUserName(name) {
35
+ return _userNames.has(name);
36
+ }
37
+ /** The raw user identifiers, for a backend that needs to allocate its own
38
+ * emitted names against them (e.g. Dafny escaping — see dafny-emit). */
39
+ export function userNames() {
40
+ return [..._userNames];
41
+ }
42
+ /** A toolchain-internal name: `base` verbatim, primed on collision. The one
43
+ * place the priming rule lives. `taken` says what counts as a collision —
44
+ * by default a user-written name anywhere in the module; callers that know
45
+ * the exact scope (e.g. a comprehension binder checking only the expressions
46
+ * it wraps) pass their own predicate. */
47
+ export function freshName(base, taken = isUserName) {
48
+ let name = base;
49
+ while (taken(name))
50
+ name += "'";
51
+ return name;
52
+ }
@@ -34,6 +34,7 @@
34
34
  * (early-return, let-cond) run in `walkStmts` so they can consume the rest
35
35
  * of the block.
36
36
  */
37
+ import { freshName } from "./names.js";
37
38
  // ── Optional-check detection ────────────────────────────────
38
39
  /** Counter for naming optChain binders. Reset per module. */
39
40
  let _ocCounter = 0;
@@ -52,7 +53,7 @@ function parseOptionalCheck(cond) {
52
53
  const hint = binderHintFor(e);
53
54
  if (hint === null)
54
55
  return null;
55
- return { scrutinee: e, innerTy, negated: true, binderHint: hint, truthiness: true };
56
+ return { scrutinee: e, innerTy, negated: true, binderHint: freshName(hint), truthiness: true };
56
57
  }
57
58
  if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
58
59
  // Bare optional truthiness: `if (e)` where e: T | undefined — true iff e is
@@ -61,7 +62,7 @@ function parseOptionalCheck(cond) {
61
62
  const hint = binderHintFor(cond);
62
63
  if (hint === null)
63
64
  return null;
64
- return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: hint, truthiness: true };
65
+ return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: freshName(hint), truthiness: true };
65
66
  }
66
67
  return null;
67
68
  }
@@ -77,7 +78,7 @@ function parseOptionalCheck(cond) {
77
78
  const hint = binderHintFor(e);
78
79
  if (hint === null)
79
80
  return null;
80
- return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: hint, truthiness: false };
81
+ return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: freshName(hint), truthiness: false };
81
82
  }
82
83
  function binderHintFor(e) {
83
84
  // Pure access paths: var(x) or field(purePath, name).
@@ -416,6 +417,26 @@ function ruleImplOptional(e) {
416
417
  ty: { kind: "bool" },
417
418
  };
418
419
  }
420
+ /** Apply an optional chain's steps (field / index / call) to a base expr —
421
+ * shared by `ruleOptChain` (base = binder) and `ruleOptChainIndex` (base = arr[i]). */
422
+ function applyChain(body, chain) {
423
+ for (const step of chain) {
424
+ if (step.kind === "field")
425
+ body = { kind: "field", obj: body, field: step.name, ty: step.ty };
426
+ else if (step.kind === "index")
427
+ body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
428
+ else
429
+ body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
430
+ }
431
+ return body;
432
+ }
433
+ /** `0 <= idx && idx < arr.length` — the in-bounds guard for an array index. */
434
+ function arrayBoundsCond(arr, idx) {
435
+ const len = { kind: "field", obj: arr, field: "length", ty: { kind: "int" } };
436
+ const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
437
+ const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
438
+ return { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
439
+ }
419
440
  /** Rule (expression): `left ?? right` — nullish coalescing.
420
441
  * → `someMatch left { Some(_v) => _v, None => right }`.
421
442
  * Single-evaluation: scrutinee may be any expression. */
@@ -425,7 +446,7 @@ function ruleNullish(e) {
425
446
  if (e.left.ty.kind !== "optional")
426
447
  return null;
427
448
  const innerTy = e.left.ty.inner;
428
- const binder = `_oc${_ocCounter++}_val`;
449
+ const binder = freshName(`_oc${_ocCounter++}_val`);
429
450
  return {
430
451
  kind: "someMatch",
431
452
  scrutinee: e.left, binder, binderTy: innerTy,
@@ -447,11 +468,7 @@ function ruleNullishIndex(e) {
447
468
  return null;
448
469
  if (e.left.obj.ty.kind !== "array")
449
470
  return null;
450
- const idx = e.left.idx;
451
- const len = { kind: "field", obj: e.left.obj, field: "length", ty: { kind: "int" } };
452
- const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
453
- const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
454
- const cond = { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
471
+ const cond = arrayBoundsCond(e.left.obj, e.left.idx);
455
472
  return { kind: "conditional", cond, then: e.left, else: e.right, ty: e.ty };
456
473
  }
457
474
  /** Rule (expression): `arr[i]?.<chain>` — optional chaining on an array index,
@@ -469,23 +486,8 @@ function ruleOptChainIndex(e) {
469
486
  return null;
470
487
  if (e.obj.obj.ty.kind !== "array")
471
488
  return null;
472
- const idx = e.obj.idx;
473
- const len = { kind: "field", obj: e.obj.obj, field: "length", ty: { kind: "int" } };
474
- const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
475
- const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
476
- const cond = { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
477
- let body = e.obj; // arr[i] — in bounds under `cond`
478
- for (const step of e.chain) {
479
- if (step.kind === "field") {
480
- body = { kind: "field", obj: body, field: step.name, ty: step.ty };
481
- }
482
- else if (step.kind === "index") {
483
- body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
484
- }
485
- else {
486
- body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
487
- }
488
- }
489
+ const cond = arrayBoundsCond(e.obj.obj, e.obj.idx);
490
+ const body = applyChain(e.obj, e.chain); // arr[i] in bounds under `cond`
489
491
  const undef = { kind: "var", name: "undefined", ty: { kind: "void" } };
490
492
  return { kind: "conditional", cond, then: body, else: undef, ty: e.ty };
491
493
  }
@@ -499,19 +501,8 @@ function ruleOptChain(e) {
499
501
  if (e.obj.ty.kind !== "optional")
500
502
  return null;
501
503
  const innerTy = e.obj.ty.inner;
502
- const binder = `_oc${_ocCounter++}_val`;
503
- let body = { kind: "var", name: binder, ty: innerTy };
504
- for (const step of e.chain) {
505
- if (step.kind === "field") {
506
- body = { kind: "field", obj: body, field: step.name, ty: step.ty };
507
- }
508
- else if (step.kind === "index") {
509
- body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
510
- }
511
- else {
512
- body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
513
- }
514
- }
504
+ const binder = freshName(`_oc${_ocCounter++}_val`);
505
+ const body = applyChain({ kind: "var", name: binder, ty: innerTy }, e.chain);
515
506
  const noneBody = { kind: "var", name: "undefined", ty: { kind: "void" } };
516
507
  return {
517
508
  kind: "someMatch",
@@ -543,9 +534,9 @@ function binderHintForMapAccess(m, k) {
543
534
  // mHint is `_m_val`, kHint is `_k_val` — stitch into `_m_k_val`.
544
535
  const mStem = mHint.replace(/_val$/, "");
545
536
  const kStem = kHint.replace(/^_/, "").replace(/_val$/, "");
546
- return `${mStem}_${kStem}_val`;
537
+ return freshName(`${mStem}_${kStem}_val`);
547
538
  }
548
- return `_oc${_ocCounter++}_val`;
539
+ return freshName(`_oc${_ocCounter++}_val`);
549
540
  }
550
541
  /** Rule (expression): `k in m ? m[k] : default` where m is map-typed.
551
542
  * The then-branch must be exactly `m[k]` (same obj, same key). This mirrors
@@ -592,9 +583,10 @@ function ruleConditionalOptionalTruthy(e) {
592
583
  return null;
593
584
  if (e.cond.ty.kind !== "optional")
594
585
  return null;
595
- const binder = binderHintFor(e.cond);
596
- if (binder === null)
586
+ const hint = binderHintFor(e.cond);
587
+ if (hint === null)
597
588
  return null;
589
+ const binder = freshName(hint);
598
590
  return {
599
591
  kind: "someMatch",
600
592
  scrutinee: e.cond, binderTy: e.cond.ty.inner,
@@ -602,31 +594,39 @@ function ruleConditionalOptionalTruthy(e) {
602
594
  someBody: e.then, noneBody: e.else, ty: e.ty,
603
595
  };
604
596
  }
605
- /** Extract an optional check from any position in an `&&` chain.
606
- * `(x !== undefined && b) && c` { check, restCond: b && c }.
607
- * `a && (x !== undefined)` → { check, restCond: a }.
608
- * Conjunct order doesn't carry semantic weight, so either side is fine. */
609
- function extractLeftmostOptionalCheck(cond) {
597
+ /** Find the leftmost `parse`-matching conjunct anywhere in an `&&` chain,
598
+ * returning it plus the remaining conjunction. Conjunct order doesn't carry
599
+ * semantic weight, so either side is fine. Shared by the optional and
600
+ * Array.isArray chain extractors they differ only in `parse`.
601
+ * `(x !== undefined && b) && c` → { check, restCond: b && c }. */
602
+ function extractLeftmostCheck(cond, parse) {
610
603
  if (cond.kind !== "binop" || cond.op !== "&&")
611
604
  return null;
612
- const leftCheck = parseSimpleOptionalCheck(cond.left);
613
- if (leftCheck && !leftCheck.negated)
614
- return { check: leftCheck, restCond: cond.right };
615
- const rightCheck = parseSimpleOptionalCheck(cond.right);
616
- if (rightCheck && !rightCheck.negated)
617
- return { check: rightCheck, restCond: cond.left };
605
+ const left = parse(cond.left);
606
+ if (left)
607
+ return { check: left, restCond: cond.right };
608
+ const right = parse(cond.right);
609
+ if (right)
610
+ return { check: right, restCond: cond.left };
618
611
  if (cond.left.kind === "binop" && cond.left.op === "&&") {
619
- const inner = extractLeftmostOptionalCheck(cond.left);
612
+ const inner = extractLeftmostCheck(cond.left, parse);
620
613
  if (inner)
621
614
  return { check: inner.check, restCond: { ...cond, left: inner.restCond } };
622
615
  }
623
616
  if (cond.right.kind === "binop" && cond.right.op === "&&") {
624
- const inner = extractLeftmostOptionalCheck(cond.right);
617
+ const inner = extractLeftmostCheck(cond.right, parse);
625
618
  if (inner)
626
619
  return { check: inner.check, restCond: { ...cond, right: inner.restCond } };
627
620
  }
628
621
  return null;
629
622
  }
623
+ /** `&&`-chain extractor for a positive optional check. */
624
+ function extractLeftmostOptionalCheck(cond) {
625
+ return extractLeftmostCheck(cond, e => {
626
+ const c = parseSimpleOptionalCheck(e);
627
+ return c && !c.negated ? c : null;
628
+ });
629
+ }
630
630
  /** Rule: `if (x !== undefined && rest) then` (no else) where x is a pure
631
631
  * access path.
632
632
  * → `someMatch x { Some(_x_val) => if rest then then; , None => {} }`.
@@ -743,31 +743,11 @@ function isNarrowablePath(e) {
743
743
  return isNarrowablePath(e.obj);
744
744
  return false;
745
745
  }
746
- /** Mirror of `extractLeftmostOptionalCheck` for synth-array-union checks:
747
- * finds `Array.isArray(path)` somewhere in a `&&` chain, returns it plus
748
- * the remaining conjunction. The check must be the positive form (negated
749
- * `!Array.isArray(...)` would narrow to the wrong variant for then-body
750
- * consumers, so we leave those to the existing untouched-conditional path). */
746
+ /** `&&`-chain extractor for `Array.isArray(path)` (positive form only — a negated
747
+ * `!Array.isArray(...)` would narrow to the wrong variant for then-body consumers,
748
+ * so those are left to the untouched-conditional path). */
751
749
  function extractLeftmostArrayIsArrayCheck(cond) {
752
- if (cond.kind !== "binop" || cond.op !== "&&")
753
- return null;
754
- const leftCheck = parseArrayIsArrayCall(cond.left);
755
- if (leftCheck)
756
- return { check: leftCheck, restCond: cond.right };
757
- const rightCheck = parseArrayIsArrayCall(cond.right);
758
- if (rightCheck)
759
- return { check: rightCheck, restCond: cond.left };
760
- if (cond.left.kind === "binop" && cond.left.op === "&&") {
761
- const inner = extractLeftmostArrayIsArrayCheck(cond.left);
762
- if (inner)
763
- return { check: inner.check, restCond: { ...cond, left: inner.restCond } };
764
- }
765
- if (cond.right.kind === "binop" && cond.right.op === "&&") {
766
- const inner = extractLeftmostArrayIsArrayCheck(cond.right);
767
- if (inner)
768
- return { check: inner.check, restCond: { ...cond, right: inner.restCond } };
769
- }
770
- return null;
750
+ return extractLeftmostCheck(cond, parseArrayIsArrayCall);
771
751
  }
772
752
  /** Detect `x.kind === "variant"`, `'key' in x`, or `Array.isArray(x)` (synth
773
753
  * array-union) as a positive discriminant check. Returns the scrutinee var
@@ -1,3 +1,4 @@
1
+ import { patternCtor, patternBinders } from "./ir.js";
1
2
  // ── Generic walkers (same shape as transform.ts) ─────────────
2
3
  function mapExpr(e, f) {
3
4
  const hit = f(e);
@@ -44,21 +45,19 @@ function isMapGet(e) {
44
45
  return null;
45
46
  return { obj: e.obj, key: e.args[0], objTy: e.objTy };
46
47
  }
47
- /** Parse Some-arm pattern like ".some _val" returns binder name, or null for ".some _" or unparseable. */
48
- function parseSomeBinder(pattern) {
49
- if (!pattern.startsWith(".some"))
48
+ /** Binder of a Some armits name, or null for `.some _` / a non-`some` pattern. */
49
+ function parseSomeBinder(p) {
50
+ if (patternCtor(p) !== "some")
50
51
  return null;
51
- const rest = pattern.slice(5).trim();
52
- if (rest === "" || rest === "_")
53
- return null;
54
- return rest.split(/\s+/)[0];
52
+ const b = patternBinders(p)[0];
53
+ return b === undefined || b === "_" ? null : b;
55
54
  }
56
55
  /** Identify a Some/None match's arms. */
57
56
  function getSomeNoneArms(arms) {
58
57
  if (arms.length !== 2)
59
58
  return null;
60
- const someArm = arms.find(a => a.pattern.startsWith(".some"));
61
- const noneArm = arms.find(a => a.pattern === ".none");
59
+ const someArm = arms.find(a => patternCtor(a.pattern) === "some");
60
+ const noneArm = arms.find(a => patternCtor(a.pattern) === "none");
62
61
  if (!someArm || !noneArm)
63
62
  return null;
64
63
  return { someArm, noneArm, binder: parseSomeBinder(someArm.pattern) };
@@ -7,6 +7,7 @@
7
7
  import { isBigInt } from "./typedir.js";
8
8
  import { parseTsType, tyToCanonical } from "./types.js";
9
9
  import { parseExpr } from "./specparser.js";
10
+ import { freshName } from "./names.js";
10
11
  function lookup(env, name) {
11
12
  if (!env)
12
13
  return undefined;
@@ -693,7 +694,7 @@ function resolveRecordMerge(base, override, ctx) {
693
694
  return { name: f.name, value: ovf }; // required: override always provides
694
695
  // optional: override field wins iff present, else base's field
695
696
  const bvf = { kind: "field", obj: bv, field: f.name, ty: ft };
696
- const binder = `_m${mergeBinder++}`;
697
+ const binder = freshName(`_m${mergeBinder++}`);
697
698
  // someBody is the unwrapped present value; transform re-wraps each arm in
698
699
  // the backend's Some constructor (Dafny `Some`, Lean `Option.some`).
699
700
  return { name: f.name, value: {
@@ -704,7 +705,7 @@ function resolveRecordMerge(base, override, ctx) {
704
705
  });
705
706
  if (tover.ty.kind === "optional") {
706
707
  // override may be absent (undefined spreads nothing) → base unchanged
707
- const binder = `_mo${mergeBinder++}`;
708
+ const binder = freshName(`_mo${mergeBinder++}`);
708
709
  return {
709
710
  kind: "someMatch", scrutinee: tover, binder, binderTy: userTy,
710
711
  someBody: merged(tbase, { kind: "var", name: binder, ty: userTy }), noneBody: tbase, ty: userTy,
@@ -1373,7 +1374,9 @@ function resolveStmt(s, ctx) {
1373
1374
  for (const _ of s.names)
1374
1375
  nameTypes.push({ kind: "unknown" });
1375
1376
  }
1376
- const idxName = `_${s.names[0]}_idx`;
1377
+ // Must match the freshened counter transform mints for this loop, so a
1378
+ // spec referencing the loop index resolves to the same name.
1379
+ const idxName = freshName(`_${s.names[0]}_idx`);
1377
1380
  env = extend(env, idxName, { kind: "nat" });
1378
1381
  for (let j = 0; j < s.names.length; j++) {
1379
1382
  env = extend(env, s.names[j], nameTypes[j] ?? { kind: "unknown" });