lemmascript 0.5.17 → 0.5.19

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.
@@ -36,3 +36,71 @@ export function runInfo(raw, outPath) {
36
36
  writeFileSync(outPath, JSON.stringify(out, null, 2) + "\n");
37
37
  console.log(`Wrote ${outPath}`);
38
38
  }
39
+ /** Statement/expression kinds present anywhere in a function body — a cheap
40
+ * structural census so consumers can classify (havoc/assume/throw usage)
41
+ * without receiving the whole body. */
42
+ function bodyKinds(fn) {
43
+ // Keys under which a Ty (not a statement/expression) hangs — their `kind`
44
+ // tags belong to the type grammar and would pollute the census.
45
+ const TY_KEYS = new Set(["ty", "binderTy", "varTy", "nameTypes"]);
46
+ const kinds = new Set();
47
+ const walk = (node) => {
48
+ if (Array.isArray(node)) {
49
+ node.forEach(walk);
50
+ return;
51
+ }
52
+ if (node && typeof node === "object") {
53
+ const o = node;
54
+ if (typeof o.kind === "string") {
55
+ kinds.add(o.kind === "assert" && o.assumed === true ? "assume" : o.kind);
56
+ }
57
+ for (const [k, v] of Object.entries(o))
58
+ if (!TY_KEYS.has(k))
59
+ walk(v);
60
+ }
61
+ };
62
+ walk(fn.body);
63
+ return [...kinds].sort();
64
+ }
65
+ function typedFn(fn, rawFn) {
66
+ // Carry the original TS type string per parameter: Ty conflates types whose
67
+ // runtime shapes differ (Map<K,V> and Record<K,V> both map to kind "map"),
68
+ // and satellites that construct runtime values need to tell them apart.
69
+ const rawTsTypes = new Map((rawFn?.params ?? []).map(p => [p.name, p.tsType]));
70
+ return {
71
+ name: fn.name,
72
+ exported: rawFn?.exported ?? false,
73
+ typeParams: fn.typeParams,
74
+ params: fn.params.map(p => ({ ...p, tsType: rawTsTypes.get(p.name) })),
75
+ returnTy: fn.returnTy,
76
+ requires: fn.requires,
77
+ ensures: fn.ensures,
78
+ decreases: fn.decreases,
79
+ contract: rawFn?.contract ?? [],
80
+ isPure: fn.isPure,
81
+ forcePure: fn.forcePure,
82
+ autohavoc: fn.autohavoc,
83
+ bodyKinds: bodyKinds(fn),
84
+ };
85
+ }
86
+ export function runTypedInfo(raw, typed, version, backendDirective, dafny) {
87
+ const rawByName = new Map(raw.functions.map(f => [f.name, f]));
88
+ const rawMethods = new Map(raw.classes.flatMap(c => c.methods.map(m => [`${c.name}.${m.name}`, m])));
89
+ const out = {
90
+ schema: 1,
91
+ lemmascript: version,
92
+ file: typed.file,
93
+ backendDirective,
94
+ typeDecls: typed.typeDecls,
95
+ externs: typed.externs,
96
+ constants: typed.constants,
97
+ functions: typed.functions.map(f => typedFn(f, rawByName.get(f.name))),
98
+ classes: typed.classes.map(c => ({
99
+ name: c.name,
100
+ fields: c.fields,
101
+ methods: c.methods.map(m => typedFn(m, rawMethods.get(`${c.name}.${m.name}`))),
102
+ })),
103
+ dafny,
104
+ };
105
+ console.log(JSON.stringify(out, null, 2));
106
+ }
package/tools/dist/ir.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * The emit phase pretty-prints them to backend syntax (Lean or Dafny).
6
6
  */
7
7
  export const pWild = () => ({ kind: "wild" });
8
- export const pCtor = (ctor, ...binders) => ({ kind: "ctor", ctor, binders });
8
+ export const pCtor = (c, ...binders) => ({ kind: "ctor", ctor: c, binders });
9
9
  /** Binder identifiers a pattern introduces (`[]` for wildcard / nullary ctor). */
10
10
  export function patternBinders(p) {
11
11
  return p.kind === "ctor" ? p.binders : [];
@@ -17,19 +17,40 @@ export function patternCtor(p) {
17
17
  export function patternBinds(p, name) {
18
18
  return patternBinders(p).includes(name);
19
19
  }
20
+ // ── Literal queries ──────────────────────────────────────────
21
+ /** The exact value of an integer-literal operand (possibly negated), or null
22
+ * when `e` isn't one. Answers in compiler-side `bigint` so a backend folding a
23
+ * literal into arithmetic stays exact: JS bitwise operators truncate to 32 bits
24
+ * and `Math.pow(2, n)` is a double, both of which lie past 2^53. A `num`
25
+ * outside the safe-integer range has already lost precision, so it is not a
26
+ * usable answer — hence null. */
27
+ export function exactIntegerLiteral(e) {
28
+ if (e.kind === "bigint")
29
+ return BigInt(e.value);
30
+ if (e.kind === "num")
31
+ return Number.isSafeInteger(e.value) ? BigInt(e.value) : null;
32
+ // Transform folds `-<num>` into a negative `num`, but leaves a negated
33
+ // `bigint` structural (folding it would coerce the payload through Number).
34
+ if (e.kind === "unop" && e.op === "-") {
35
+ const inner = exactIntegerLiteral(e.expr);
36
+ return inner === null ? null : -inner;
37
+ }
38
+ return null;
39
+ }
20
40
  export function anyExpr(e, pred) {
21
41
  if (pred(e))
22
42
  return true;
23
43
  switch (e.kind) {
24
44
  case "var":
25
45
  case "num":
46
+ case "bigint":
26
47
  case "bool":
27
48
  case "str":
28
49
  case "emptyMap":
29
50
  case "emptySet":
30
51
  case "havoc":
31
52
  case "default": return false;
32
- case "constructor": return (e.args ?? []).some(a => anyExpr(a, pred));
53
+ case "constructor": return e.args.some(a => anyExpr(a, pred));
33
54
  case "binop": return anyExpr(e.left, pred) || anyExpr(e.right, pred);
34
55
  case "unop":
35
56
  case "toNat":
@@ -46,7 +67,7 @@ export function anyExpr(e, pred) {
46
67
  case "methodCall": return anyExpr(e.obj, pred) || e.args.some(a => anyExpr(a, pred));
47
68
  case "lambda": return e.body.some(s => anyExprInStmt(s, pred));
48
69
  case "if": return anyExpr(e.cond, pred) || anyExpr(e.then, pred) || anyExpr(e.else, pred);
49
- case "match": return (typeof e.scrutinee !== "string" && anyExpr(e.scrutinee, pred)) || e.arms.some(a => anyExpr(a.body, pred));
70
+ case "match": return anyExpr(e.scrutinee, pred) || e.arms.some(a => anyExpr(a.body, pred));
50
71
  case "forall":
51
72
  case "exists": return anyExpr(e.body, pred);
52
73
  case "let": return anyExpr(e.value, pred) || anyExpr(e.body, pred);
@@ -65,7 +86,7 @@ export function anyExprInStmt(s, pred) {
65
86
  case "break":
66
87
  case "continue": return false;
67
88
  case "if": return anyExpr(s.cond, pred) || anyExprInStmts(s.then, pred) || anyExprInStmts(s.else, pred);
68
- case "match": return (typeof s.scrutinee !== "string" && anyExpr(s.scrutinee, pred)) || s.arms.some(a => anyExprInStmts(a.body, pred));
89
+ case "match": return anyExpr(s.scrutinee, pred) || s.arms.some(a => anyExprInStmts(a.body, pred));
69
90
  case "while": return anyExpr(s.cond, pred) || s.invariants.some(i => anyExpr(i, pred))
70
91
  || (s.decreasing ? anyExpr(s.decreasing, pred) : false) || (s.doneWith ? anyExpr(s.doneWith, pred) : false)
71
92
  || anyExprInStmts(s.body, pred);
@@ -80,10 +101,9 @@ export function anyExprInStmts(stmts, pred) {
80
101
  // function. These drive the *local* freshness checks for user-facing binders
81
102
  // (the result out-parameter, comprehension binders): a binder is checked only
82
103
  // against the expressions/scope it actually wraps, not the whole module.
83
- const _refsName = (name) => e => (e.kind === "var" && e.name === name) ||
104
+ const _refsName = (name) => (e) => (e.kind === "var" && e.name === name) ||
84
105
  (e.kind === "app" && e.fn === name) ||
85
- (e.kind === "constructor" && e.name === name) ||
86
- (e.kind === "match" && typeof e.scrutinee === "string" && e.scrutinee === name);
106
+ (e.kind === "constructor" && e.name === name);
87
107
  export function usesName(e, name) {
88
108
  return anyExpr(e, _refsName(name));
89
109
  }
@@ -5,7 +5,7 @@
5
5
  * and support-import selection.
6
6
  */
7
7
  import { anyExpr, usesNameInDecl, patternBinders } from "./ir.js";
8
- import { freshName } from "./names.js";
8
+ import { freshNameWhere } from "./names.js";
9
9
  // ── Ty → Lean type string ──────────────────────────────────
10
10
  function tyToLean(ty) {
11
11
  switch (ty.kind) {
@@ -190,9 +190,15 @@ let _unknownEmitted = false; // across files in one run — the def file imports
190
190
  // built only from decidable atoms (comparisons, Bool-returning calls) coerces fine
191
191
  // and stays in the more proof-friendly Prop form.
192
192
  let _boolCtx = false;
193
+ /** Constructor names come from source strings (string-union values,
194
+ * discriminated-union tags) and may contain non-identifier characters
195
+ * ("spec-pure"); guillemet-quote those — exact and collision-free. */
196
+ function leanCtorName(name) {
197
+ return /^[A-Za-z_][A-Za-z0-9_'!?]*$/.test(name) ? name : `«${name}»`;
198
+ }
193
199
  /** Render a match pattern to Lean syntax: `_`, `.none`, `.some x`, `.syn seq`. */
194
200
  function renderLeanPattern(p) {
195
- return p.kind === "wild" ? "_" : "." + [p.ctor, ...p.binders].join(" ");
201
+ return p.kind === "wild" ? "_" : "." + [leanCtorName(p.ctor), ...p.binders].join(" ");
196
202
  }
197
203
  // A Bool-valued atom that does NOT coerce to Prop: an inlined union discriminator
198
204
  // (lowered to a match-bool `match x with | .C .. => true | _ => false`) or a raw
@@ -313,6 +319,8 @@ function emitExpr(e, parentPrec) {
313
319
  // `undefined` is the IR's spelling of the absent optional (mirrors dafny-emit's None)
314
320
  case "var": return e.name === "undefined" ? "none" : escapeName(e.name);
315
321
  case "num": return `${e.value}`;
322
+ // Already canonical decimal; Lean's `Int` is mathematical, so no `n` suffix.
323
+ case "bigint": return e.value;
316
324
  case "bool": return e.value ? "true" : "false";
317
325
  case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
318
326
  case "constructor": {
@@ -320,7 +328,7 @@ function emitExpr(e, parentPrec) {
320
328
  // like `match ... | .none => Type.some x` where elaboration can't infer).
321
329
  // Without type: emit `.name` (dotted form; works in pattern positions
322
330
  // and where the expected type is clear from context).
323
- const head = e.type ? `${e.type}.${e.name}` : `.${e.name}`;
331
+ const head = e.type ? `${e.type}.${leanCtorName(e.name)}` : `.${leanCtorName(e.name)}`;
324
332
  if (!e.args || e.args.length === 0)
325
333
  return head;
326
334
  const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app" || a.kind === "methodCall") ? `(${emitExpr(a)})` : emitExpr(a));
@@ -361,9 +369,10 @@ function emitExpr(e, parentPrec) {
361
369
  case "unop":
362
370
  if (e.op === "¬")
363
371
  return _boolCtx ? `!(${emitExpr(e.expr)})` : `¬(${emitExpr(e.expr)})`;
364
- if (e.op === "-" && e.expr.kind === "num")
365
- return `-${e.expr.value}`;
366
- return `(-${emitExpr(e.expr)})`;
372
+ if (e.op !== "-")
373
+ throw new Error(`Unsupported Lean unary operator: ${e.op}`);
374
+ return (e.expr.kind === "num" || e.expr.kind === "bigint")
375
+ ? `-${e.expr.value}` : `(-${emitExpr(e.expr)})`;
367
376
  case "binop": {
368
377
  // Discriminator test against a constructor that carries fields:
369
378
  // `x = .Ctor` → `(match x with | .Ctor .. => true | _ => false)`. A
@@ -415,8 +424,10 @@ function emitExpr(e, parentPrec) {
415
424
  // Datatype constructor (tagged by transform): Lean needs the qualified name
416
425
  // `BaseType.variant`; a bare `variant` is an unknown identifier. (Dafny keeps
417
426
  // the bare form, so its output is unaffected.)
418
- if (e.ctorOf)
419
- return args.length ? `${e.ctorOf}.${e.fn} ${args.join(" ")}` : `${e.ctorOf}.${e.fn}`;
427
+ if (e.ctorOf) {
428
+ const ctor = leanCtorName(e.fn);
429
+ return args.length ? `${e.ctorOf}.${ctor} ${args.join(" ")}` : `${e.ctorOf}.${ctor}`;
430
+ }
420
431
  // Option constructors arrive Dafny-spelled from transform (`app "Some"`);
421
432
  // Lean core exports the lowercase forms as top-level names.
422
433
  if (e.fn === "Some" && args.length === 1)
@@ -466,12 +477,12 @@ function emitExpr(e, parentPrec) {
466
477
  const obj = emitExpr(e.obj);
467
478
  if (e.field === "collectionSize")
468
479
  return `${obj}.size`;
469
- const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bool";
480
+ const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bigint" && e.obj.kind !== "bool";
470
481
  return wrap ? `(${obj}).${escapeName(e.field)}` : `${obj}.${escapeName(e.field)}`;
471
482
  }
472
483
  case "toNat": {
473
484
  const inner = emitExpr(e.expr);
474
- const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
485
+ const wrap = e.expr.kind !== "var" && e.expr.kind !== "num" && e.expr.kind !== "bigint";
475
486
  return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
476
487
  }
477
488
  case "toReal":
@@ -498,7 +509,7 @@ function emitExpr(e, parentPrec) {
498
509
  // so any token after an arm body (`→`, another match's `|`, etc.) would
499
510
  // bleed into the last `.none` case without explicit bracketing.
500
511
  const arms = e.arms.map(a => `| ${renderLeanPattern(a.pattern)} => ${emitExpr(a.body)}`);
501
- const scrut = typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee);
512
+ const scrut = emitExpr(e.scrutinee);
502
513
  return `(match ${scrut} with ${arms.join(" ")})`;
503
514
  }
504
515
  case "forall": return `∀ ${e.var} : ${tyToLean(e.type)}, ${emitExpr(e.body)}`;
@@ -586,7 +597,7 @@ function emitStmt(s, indent) {
586
597
  return out;
587
598
  }
588
599
  case "match": {
589
- const scrut = typeof s.scrutinee === "string" ? s.scrutinee : emitExpr(s.scrutinee);
600
+ const scrut = emitExpr(s.scrutinee);
590
601
  // Option match (.some/.none) → emit as if/let for WPGen.if compatibility
591
602
  if (s.arms.length === 2) {
592
603
  const someArm = s.arms.find(a => a.pattern.kind === "ctor" && a.pattern.ctor === "some");
@@ -718,11 +729,11 @@ function emitDecl(d) {
718
729
  const lines = [`inductive ${d.name} where`];
719
730
  for (const c of d.constructors) {
720
731
  if (c.fields.length === 0) {
721
- lines.push(` | ${c.name} : ${d.name}`);
732
+ lines.push(` | ${leanCtorName(c.name)} : ${d.name}`);
722
733
  }
723
734
  else {
724
735
  const params = c.fields.map(f => `(${escapeName(f.name)} : ${tyToLean(f.type)})`).join(" ");
725
- lines.push(` | ${c.name} ${params} : ${d.name}`);
736
+ lines.push(` | ${leanCtorName(c.name)} ${params} : ${d.name}`);
726
737
  }
727
738
  }
728
739
  return lines.join("\n") + emitDeriving(d.name, d.deriving);
@@ -775,7 +786,7 @@ function emitDecl(d) {
775
786
  // Prime the return binder only on a collision within *this method's own*
776
787
  // signature/body — `res` is a common identifier module-wide (record
777
788
  // fields, unrelated params), so a module-wide check would prime spuriously.
778
- _resultName = freshName("res", n => d.params.some(p => escapeName(p.name) === n) ||
789
+ _resultName = freshNameWhere("res", n => d.params.some(p => escapeName(p.name) === n) ||
779
790
  usesNameInDecl(d.requires, d.ensures, d.body, n));
780
791
  const lines = [`method ${d.name} ${params} return (${_resultName} : ${tyToLean(d.returnType)})`];
781
792
  for (const r of d.requires)
@@ -829,7 +840,7 @@ function emitPureExpr(e, indent) {
829
840
  case "if":
830
841
  return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
831
842
  case "match": {
832
- const lines = [`${pad}match ${typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee)} with`];
843
+ const lines = [`${pad}match ${emitExpr(e.scrutinee)} with`];
833
844
  for (const arm of e.arms) {
834
845
  lines.push(`${pad}| ${renderLeanPattern(arm.pattern)} =>`);
835
846
  lines.push(emitPureExpr(arm.body, indent + 1));
package/tools/dist/lsc.js CHANGED
@@ -16,12 +16,25 @@ import { autoHavocModule } from "./autohavoc.js";
16
16
  import { transformModuleLean, transformModuleDafny } from "./transform.js";
17
17
  import { peepholeModule } from "./peephole.js";
18
18
  import { emitLeanFile, resetLeanModule } from "./lean-emit.js";
19
- import { emitDafnyFile } from "./dafny-emit.js";
19
+ import { emitDafnyFile, emittedNameMap } from "./dafny-emit.js";
20
20
  import { dafnyGen, dafnyCheckDiff, dafnyVerify, dafnyRegen } from "./dafny-commands.js";
21
21
  import { leanGen, leanCheck } from "./lean-commands.js";
22
- import { runInfo } from "./info-command.js";
22
+ import { runInfo, runTypedInfo } from "./info-command.js";
23
+ /** Version of the lemmascript package — the root package.json sits two levels
24
+ * above this module from both tools/src/ (tsx) and tools/dist/ (installed). */
25
+ function lscVersion() {
26
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
27
+ return pkg.version;
28
+ }
23
29
  function main() {
24
30
  const args = process.argv.slice(2);
31
+ // `lsc version` — print the package version. Machine consumers (satellites
32
+ // like lemmascript-claimcheck/-crosscheck) use this for their version
33
+ // handshake; keep the output to the bare semver string.
34
+ if (args[0] === "version") {
35
+ console.log(lscVersion());
36
+ return;
37
+ }
25
38
  // `lsc claimcheck <file.ts> …` forwards verbatim to the lemmascript-claimcheck
26
39
  // CLI (a dependency; its cli reads the rewritten process.argv). With no
27
40
  // leading <file.ts>, batch: one satellite run per LemmaScript-files.txt entry,
@@ -108,6 +121,14 @@ function main() {
108
121
  noVerify = true;
109
122
  args.splice(noVerifyIdx, 1);
110
123
  }
124
+ // --typed (info only): print the machine-readable Typed IR contract to
125
+ // stdout instead of writing the human-oriented foo.ts.json.
126
+ let typedInfo = false;
127
+ const typedIdx = args.indexOf("--typed");
128
+ if (typedIdx >= 0) {
129
+ typedInfo = true;
130
+ args.splice(typedIdx, 1);
131
+ }
111
132
  // Anything flag-shaped left over is a typo or a space-separated form
112
133
  // (`--backend lean`): reject it rather than let it become a positional arg
113
134
  // or be silently ignored (which would e.g. verify with the wrong backend).
@@ -119,15 +140,21 @@ function main() {
119
140
  const [cmd, filePath] = args;
120
141
  if (!cmd) {
121
142
  console.error("Usage: lsc <gen|check|regen|extract|info> [--backend=lean|dafny] <file.ts>");
143
+ console.error(" lsc info --typed <file.ts> (machine-readable Typed IR contract to stdout)");
122
144
  console.error(" lsc <gen|gen-check|check> [--backend=…] [--slow] (no file: batch over LemmaScript-files.txt)");
123
145
  console.error(" lsc claimcheck [<file.ts>] [flags…] (forwards to lemmascript-claimcheck)");
146
+ console.error(" lsc version");
147
+ process.exit(1);
148
+ }
149
+ if (typedInfo && cmd !== "info") {
150
+ console.error(`--typed is only valid with the info command (got: ${cmd})`);
124
151
  process.exit(1);
125
152
  }
126
153
  if (!filePath) {
127
154
  runBatch(cmd, backend, slow);
128
155
  return;
129
156
  }
130
- runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify);
157
+ runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify, typedInfo);
131
158
  }
132
159
  // LemmaScript-files.txt, parsed: `filepath [timeout_in_seconds] [extra dafny
133
160
  // flags…]` per line; no timeout = Dafny default. Exits if the file is absent.
@@ -164,7 +191,7 @@ function runBatch(cmd, backend, slow) {
164
191
  }
165
192
  }
166
193
  }
167
- function runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify = false) {
194
+ function runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify = false, typedInfo = false) {
168
195
  const absPath = path.resolve(filePath);
169
196
  if (!existsSync(absPath)) {
170
197
  console.error(`File not found: ${absPath}`);
@@ -212,7 +239,7 @@ function runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify = false
212
239
  console.log(JSON.stringify(raw, null, 2));
213
240
  return;
214
241
  }
215
- if (cmd === "info") {
242
+ if (cmd === "info" && !typedInfo) {
216
243
  const outPath = path.join(path.dirname(absPath), `${path.basename(filePath, ".ts")}.ts.json`);
217
244
  runInfo(raw, outPath);
218
245
  return;
@@ -224,6 +251,27 @@ function runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify = false
224
251
  // values so verification rests only on the declared contracts (a sound
225
252
  // over-approximation). No-op unless a function opts in.
226
253
  const typed = autoHavocModule(narrowModule(resolved));
254
+ if (cmd === "info") {
255
+ // --typed: the satellite contract. Run an in-memory Dafny emission purely
256
+ // to harvest the emitted-name map; the file is backend-neutral otherwise,
257
+ // so a failure here (e.g. Lean-only constructs) degrades to an error note
258
+ // rather than failing the command.
259
+ let dafnyInfo;
260
+ try {
261
+ let { typesFile, defFile } = transformModuleDafny(typed);
262
+ if (typesFile)
263
+ typesFile = peepholeModule(typesFile, "dafny");
264
+ defFile = peepholeModule(defFile, "dafny");
265
+ const merged = { ...defFile, decls: [...(typesFile?.decls ?? []), ...defFile.decls] };
266
+ emitDafnyFile(merged, path.basename(filePath), { safeSlice });
267
+ dafnyInfo = { emittedNames: Object.fromEntries(emittedNameMap()) };
268
+ }
269
+ catch (err) {
270
+ dafnyInfo = { error: err instanceof Error ? err.message : String(err) };
271
+ }
272
+ runTypedInfo(raw, typed, lscVersion(), backendDirective ? backendDirective[1] : null, dafnyInfo);
273
+ return;
274
+ }
227
275
  const dir = path.dirname(absPath);
228
276
  const base = path.basename(filePath, ".ts");
229
277
  // ── Dafny backend ─────────────────────────────────────────
@@ -39,12 +39,16 @@ export function isUserName(name) {
39
39
  export function userNames() {
40
40
  return [..._userNames];
41
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) {
42
+ /** A toolchain-internal name: `base` verbatim, primed on collision against
43
+ * user-written names anywhere in the module. The common form deterministic
44
+ * per module. */
45
+ export function freshName(base) {
46
+ return freshNameWhere(base, isUserName);
47
+ }
48
+ /** The priming rule, against a caller-chosen collision predicate — for
49
+ * callers that know the exact scope (e.g. a comprehension binder checking
50
+ * only the expressions it wraps). The one place the rule lives. */
51
+ export function freshNameWhere(base, taken) {
48
52
  let name = base;
49
53
  while (taken(name))
50
54
  name += "'";