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.
- package/package.json +2 -2
- package/tools/dist/dafny-commands.js +7 -4
- package/tools/dist/dafny-emit.js +138 -48
- package/tools/dist/extract.js +6 -2
- package/tools/dist/ir.js +57 -0
- package/tools/dist/lean-emit.js +26 -10
- package/tools/dist/lsc.js +15 -2
- package/tools/dist/names.js +52 -0
- package/tools/dist/narrow.js +60 -80
- package/tools/dist/peephole.js +8 -9
- package/tools/dist/resolve.js +6 -3
- package/tools/dist/transform.js +104 -52
|
@@ -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
|
+
}
|
package/tools/dist/narrow.js
CHANGED
|
@@ -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
|
|
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
|
|
473
|
-
const
|
|
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
|
-
|
|
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
|
|
596
|
-
if (
|
|
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
|
-
/**
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
|
|
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
|
|
613
|
-
if (
|
|
614
|
-
return { check:
|
|
615
|
-
const
|
|
616
|
-
if (
|
|
617
|
-
return { check:
|
|
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 =
|
|
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 =
|
|
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
|
-
/**
|
|
747
|
-
*
|
|
748
|
-
*
|
|
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
|
-
|
|
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
|
package/tools/dist/peephole.js
CHANGED
|
@@ -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
|
-
/**
|
|
48
|
-
function parseSomeBinder(
|
|
49
|
-
if (
|
|
48
|
+
/** Binder of a Some arm — its name, or null for `.some _` / a non-`some` pattern. */
|
|
49
|
+
function parseSomeBinder(p) {
|
|
50
|
+
if (patternCtor(p) !== "some")
|
|
50
51
|
return null;
|
|
51
|
-
const
|
|
52
|
-
|
|
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
|
|
61
|
-
const noneArm = arms.find(a => a.pattern === "
|
|
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) };
|
package/tools/dist/resolve.js
CHANGED
|
@@ -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
|
-
|
|
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" });
|