lemmascript 0.5.18 → 0.5.20

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.
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Dafny emitter — IR → Dafny text.
3
3
  */
4
- import { usesName, usesNameInDecl } from "./ir.js";
5
- import { freshName, userNames } from "./names.js";
4
+ import { exactIntegerLiteral, usesName, usesNameInDecl, usesNameInStmts } from "./ir.js";
5
+ import { freshNameWhere, userNames } from "./names.js";
6
6
  import { renameFreeVar } from "./transform.js";
7
7
  /** Fresh binder for a comprehension wrapping the given subexpressions: `base`
8
8
  * verbatim unless one of them references it, then primed until free. A *local*
9
9
  * check — a same-named name elsewhere in the module keeps the plain binder. */
10
10
  function freshBinder(base, ...wrapped) {
11
- return freshName(base, name => wrapped.some(w => usesName(w, name)));
11
+ return freshNameWhere(base, name => wrapped.some(w => usesName(w, name)));
12
12
  }
13
13
  /** Binder + body for lowering a single-return lambda to a comprehension whose
14
14
  * receiver is emitted inside the binder's scope. TS scoping keeps the receiver
@@ -80,6 +80,24 @@ const DAFNY_KEYWORDS = new Set([
80
80
  // by methodHeader and reset per decl. `\result` in an ensures must use the
81
81
  // *same* name, so escapeName routes it here.
82
82
  let _resultName = "res";
83
+ // User-type names appearing as the type of a havoc anywhere in the module —
84
+ // populated per file by emitDafnyFile, read by the opaque-type case.
85
+ const _havocedTypeNames = new Set();
86
+ /** Collect the user-type names of every havoc in a decl tree. */
87
+ function collectHavocedTypeNames(v, out) {
88
+ if (Array.isArray(v)) {
89
+ for (const x of v)
90
+ collectHavocedTypeNames(x, out);
91
+ return;
92
+ }
93
+ if (v === null || typeof v !== "object")
94
+ return;
95
+ const n = v;
96
+ if (n.kind === "havoc" && n.type?.kind === "user")
97
+ out.add(n.type.name);
98
+ for (const x of Object.values(v))
99
+ collectHavocedTypeNames(x, out);
100
+ }
83
101
  // ── Dafny name allocation ──────────────────────────────────
84
102
  //
85
103
  // freshName (names.ts) freshens in the *raw TS* namespace — but that is not the
@@ -128,6 +146,13 @@ function resetDafnyNameCache() {
128
146
  _takenDafnyNames.add(emitted);
129
147
  }
130
148
  }
149
+ /** Source-name → emitted-Dafny-name pairs from the most recent emitDafnyFile
150
+ * call (identity-mapped names included). Consumed by `lsc info --typed` so
151
+ * satellites (e.g. lemmascript-crosscheck) can address emitted declarations
152
+ * by their mangled names (`_Box` → `i_Box'`) without re-deriving the rules. */
153
+ export function emittedNameMap() {
154
+ return new Map([..._generatedDafnyNames, ..._userDafnyNames]);
155
+ }
131
156
  function escapeName(name) {
132
157
  // \result is carried through the IR as var "\\result"; render it as the
133
158
  // current method's out-parameter name (chosen locally by methodHeader).
@@ -138,6 +163,13 @@ function escapeName(name) {
138
163
  return user;
139
164
  return escapeGeneratedName(name);
140
165
  }
166
+ function isEmittedUserName(name) {
167
+ for (const emitted of _userDafnyNames.values()) {
168
+ if (emitted === name)
169
+ return true;
170
+ }
171
+ return false;
172
+ }
141
173
  /** Allocate a toolchain-generated name (an ANF temp, a comprehension binder, a
142
174
  * companion `_ensures` lemma). Escapes to a base, then freshens in the Dafny
143
175
  * namespace so it can't collapse onto an escaped user name. Bypasses the user
@@ -171,7 +203,7 @@ function methodHeader(prefix, params, returnType, scope) {
171
203
  // name is recorded so `\result` references resolve to it.
172
204
  const taken = (n) => params.some(p => escapeName(p.name) === n) ||
173
205
  (scope !== undefined && usesNameInDecl(scope.requires, scope.ensures, scope.body, n));
174
- const resName = freshName("res", taken);
206
+ const resName = freshNameWhere("res", taken);
175
207
  _resultName = resName;
176
208
  return `${sig} returns (${resName}: ${tyToDafny(returnType)})`;
177
209
  }
@@ -183,9 +215,8 @@ const OP_MAP = {
183
215
  };
184
216
  function mapOp(op) { return OP_MAP[op] ?? op; }
185
217
  // ── Expression emission ─────────────────────────────────────
186
- /** Emit a match scrutinee — either a variable name (string) or an expression. */
187
218
  function emitScrutinee(s) {
188
- return typeof s === "string" ? escapeName(s) : emitExpr(s);
219
+ return emitExpr(s);
189
220
  }
190
221
  /** Collapse nested forall/exists into a single quantifier with multiple bound vars. */
191
222
  function emitQuantifier(e, keyword) {
@@ -212,6 +243,8 @@ function emitExpr(e) {
212
243
  switch (e.kind) {
213
244
  case "var": return e.name === "undefined" ? "None" : escapeName(e.name);
214
245
  case "num": return `${e.value}`;
246
+ // Already canonical decimal; Dafny's `int` is mathematical, so no `n` suffix.
247
+ case "bigint": return e.value;
215
248
  case "bool": return e.value ? "true" : "false";
216
249
  case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
217
250
  case "constructor": {
@@ -281,8 +314,43 @@ function emitExpr(e) {
281
314
  }
282
315
  return `${obj}[${args[0]}..${args[1]}]`;
283
316
  }
284
- if (e.method === "map")
285
- return `Std.Collections.Seq.Map(${args[0]}, ${obj})`;
317
+ if (e.method === "map") {
318
+ // Always a seq comprehension: Seq.Map would hide the element access
319
+ // behind a closure, defeating Dafny's termination checker for
320
+ // recursive rebuild walkers. A literal lambda argument is
321
+ // beta-reduced through a `var` binding — an applied closure defeats
322
+ // the checker too; any other argument is applied to the element
323
+ // directly. A non-variable receiver is bound once up front: the
324
+ // comprehension mentions it three times, and splicing would make
325
+ // any closure literal inside it (e.g. a Filter predicate) three
326
+ // distinct closures, unprovably equal through an opaque callee.
327
+ // Name freshness is a local IR-level check until a backend name
328
+ // allocator exists.
329
+ const lam = e.args[0];
330
+ const fresh = (base, taken) => {
331
+ let name = base;
332
+ for (let n = 2; taken(name); n++)
333
+ name = `${base}${n}`;
334
+ return name;
335
+ };
336
+ const taken = (n) => usesName(e.obj, n) || usesName(lam, n) ||
337
+ (lam.kind === "lambda" && (lam.params.some(pp => pp.name === n) ||
338
+ usesNameInStmts(lam.body, n)));
339
+ const bind = e.obj.kind !== "var";
340
+ const s = bind ? fresh("s_map", taken) : obj;
341
+ const idx = fresh("i_map", n => taken(n) || n === s);
342
+ let core;
343
+ if (lam.kind === "lambda" && lam.params.length === 1 &&
344
+ lam.body.length === 1 && lam.body[0].kind === "return") {
345
+ const p = escapeName(lam.params[0].name);
346
+ core = `var ${p} := ${s}[${idx}]; ${emitExpr(lam.body[0].value)}`;
347
+ }
348
+ else {
349
+ core = `(${args[0]})(${s}[${idx}])`;
350
+ }
351
+ const comp = `seq(|${s}|, ${idx} requires 0 <= ${idx} < |${s}| => ${core})`;
352
+ return bind ? `(var ${s} := ${obj}; ${comp})` : comp;
353
+ }
286
354
  if (e.method === "filter")
287
355
  return `Std.Collections.Seq.Filter(${args[0]}, ${obj})`;
288
356
  // filterMap (synthesized in resolve): drop Nones and unwrap to seq<T>.
@@ -293,6 +361,11 @@ function emitExpr(e) {
293
361
  }
294
362
  if (e.method === "every")
295
363
  return `Std.Collections.Seq.All(${obj}, ${args[0]})`;
364
+ if (e.method === "find") {
365
+ needPreamble("OptionType");
366
+ needPreamble("SeqFind");
367
+ return `SeqFind(${obj}, ${args[0]})`;
368
+ }
296
369
  if (e.method === "findLast") {
297
370
  needPreamble("OptionType");
298
371
  needPreamble("SeqFindLast");
@@ -348,10 +421,14 @@ function emitExpr(e) {
348
421
  return `StringSplit(${obj}, ${args[0]})`;
349
422
  }
350
423
  if (e.method === "slice") {
351
- // JS negative index: arr.slice(0, -N) → arr[0..|arr|-N]. After
352
- // transform, unary minus on a numeric literal is folded to a
353
- // negative `num` IR node, so check for that here.
354
- const negVal = (a) => a.kind === "num" && a.value < 0 ? -a.value : null;
424
+ // JS negative index: arr.slice(0, -N) → arr[0..|arr|-N]. Transform
425
+ // folds unary minus on a numeric literal into a negative `num` node,
426
+ // but leaves a negated bigint structural `exactIntegerLiteral`
427
+ // recognizes both.
428
+ const negVal = (a) => {
429
+ const v = exactIntegerLiteral(a);
430
+ return v !== null && v < 0n ? (-v).toString(10) : null;
431
+ };
355
432
  const loN = negVal(e.args[0]);
356
433
  const loEx = loN !== null ? `|${obj}|-${loN}` : args[0];
357
434
  if (args.length === 1)
@@ -395,6 +472,10 @@ function emitExpr(e) {
395
472
  return `(|${obj}| >= |${args[0]}| && ${obj}[..|${args[0]}|] == ${args[0]})`;
396
473
  if (e.method === "charCodeAt")
397
474
  return `(${obj}[${args[0]}] as int)`;
475
+ if (e.method === "repeat") {
476
+ needPreamble("StringRepeat");
477
+ return `StringRepeat(${obj}, ${args[0]})`;
478
+ }
398
479
  }
399
480
  // Map methods
400
481
  if (ty === "map") {
@@ -453,6 +534,8 @@ function emitExpr(e) {
453
534
  return `!(${emitExpr(e.expr)})`;
454
535
  if (e.op === "-" && e.expr.kind === "num")
455
536
  return `(-(${e.expr.value}))`;
537
+ if (e.op === "-" && e.expr.kind === "bigint")
538
+ return `(-(${e.expr.value}))`;
456
539
  if (e.op === "-")
457
540
  return `(-(${emitExpr(e.expr)}))`;
458
541
  return `${op}(${emitExpr(e.expr)})`;
@@ -461,34 +544,30 @@ function emitExpr(e) {
461
544
  // Discriminant check: x == .Ctor → x.Ctor?
462
545
  const op = mapOp(e.op);
463
546
  if ((op === "==" || op === "!=") && e.right.kind === "constructor") {
464
- const ctorName = escapeName(e.right.name.replace(/^\./, ""));
547
+ const ctorName = dafnyCtorName(e.right.name.replace(/^\./, ""));
465
548
  const pred = `${emitExpr(e.left)}.${ctorName}?`;
466
549
  return op === "!=" ? `(!${pred})` : pred;
467
550
  }
468
551
  // Bitwise operators on int: translate to arithmetic
469
552
  // x >> n → x / 2^n (right shift)
470
553
  // x << n → x * 2^n (left shift)
471
- if (e.op === ">>") {
472
- if (e.right.kind === "num") {
473
- return `(${emitExpr(e.left)} / ${Math.pow(2, e.right.value)})`;
474
- }
475
- needPreamble("Pow2");
476
- return `(${emitExpr(e.left)} / Pow2(${emitExpr(e.right)}))`;
477
- }
478
- if (e.op === "<<") {
479
- if (e.right.kind === "num") {
480
- return `(${emitExpr(e.left)} * ${Math.pow(2, e.right.value)})`;
554
+ if (e.op === ">>" || e.op === "<<") {
555
+ const shift = exactIntegerLiteral(e.right);
556
+ // Cap the fold: a huge literal shift would inline an absurd numeral.
557
+ if (shift !== null && shift >= 0n && shift <= 1024n) {
558
+ const factor = (1n << shift).toString(10);
559
+ return `(${emitExpr(e.left)} ${e.op === ">>" ? "/" : "*"} ${factor})`;
481
560
  }
482
561
  needPreamble("Pow2");
483
- return `(${emitExpr(e.left)} * Pow2(${emitExpr(e.right)}))`;
562
+ return `(${emitExpr(e.left)} ${e.op === ">>" ? "/" : "*"} Pow2(${emitExpr(e.right)}))`;
484
563
  }
485
564
  // x & mask → x % (mask + 1) for literal masks of form 2^n - 1, else BitAnd
486
565
  if (e.op === "&") {
487
- if (e.right.kind === "num") {
488
- const mask = e.right.value;
489
- const modulus = mask + 1;
490
- if ((modulus & (modulus - 1)) === 0) {
491
- return `(${emitExpr(e.left)} % ${modulus})`;
566
+ const mask = exactIntegerLiteral(e.right);
567
+ if (mask !== null && mask >= 0n) {
568
+ const modulus = mask + 1n;
569
+ if ((modulus & (modulus - 1n)) === 0n) {
570
+ return `(${emitExpr(e.left)} % ${modulus.toString(10)})`;
492
571
  }
493
572
  }
494
573
  needPreamble("BitAnd");
@@ -532,6 +611,8 @@ function emitExpr(e) {
532
611
  needPreamble("CeilReal");
533
612
  if (e.fn === "FloorReal")
534
613
  needPreamble("FloorReal");
614
+ if (e.fn === "StringFromCharCode")
615
+ needPreamble("StringFromCharCode");
535
616
  if (e.fn === "NatToString")
536
617
  needPreamble("NatToString");
537
618
  if (e.fn === "IntToString") {
@@ -556,6 +637,20 @@ function emitExpr(e) {
556
637
  needPreamble("Perm");
557
638
  if (e.fn === "SetFromSeq")
558
639
  needPreamble("SetFromSeq");
640
+ // A constructor application must spell the name the same way the
641
+ // datatype declaration does — `dafnyCtorName`, not `escapeName`, since
642
+ // tags come from source strings ("spec-pure") that escapeName leaves alone.
643
+ if (e.ctorOf) {
644
+ const ctor = dafnyCtorName(e.fn);
645
+ // A source local may have the same name as a discriminated-union
646
+ // variant (`const error = ...; return { kind: "error", error }`).
647
+ // The bare `error(error)` is then parsed as a call through the local.
648
+ // Qualify whenever the emitted constructor spelling is already claimed
649
+ // in the user namespace, as well as when two datatypes share it.
650
+ return _ambiguousCtors.has(e.fn) || isEmittedUserName(ctor)
651
+ ? `${e.ctorOf}.${ctor}(${args.join(", ")})`
652
+ : `${ctor}(${args.join(", ")})`;
653
+ }
559
654
  return `${escapeName(e.fn)}(${args.join(", ")})`;
560
655
  }
561
656
  case "field": {
@@ -568,6 +663,11 @@ function emitExpr(e) {
568
663
  return `${obj}.Keys`;
569
664
  if (e.field === "toNat")
570
665
  return obj;
666
+ if (e.ctor && e.fromUnion) {
667
+ const renamed = _ctorFieldRenames.get(`${e.fromUnion}.${e.ctor}.${e.field}`);
668
+ if (renamed)
669
+ return `${obj}.${escapeName(renamed)}`;
670
+ }
571
671
  return `${obj}.${escapeName(e.field)}`;
572
672
  }
573
673
  case "toNat":
@@ -588,7 +688,10 @@ function emitExpr(e) {
588
688
  if (e.fields.length === 0) {
589
689
  return emitExpr(e.spread);
590
690
  }
591
- const updates = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
691
+ const updates = e.fields.map(f => {
692
+ const renamed = e.ctor && e.ctorOf ? _ctorFieldRenames.get(`${e.ctorOf}.${e.ctor}.${f.name}`) : undefined;
693
+ return `${escapeName(renamed ?? f.name)} := ${emitExpr(f.value)}`;
694
+ });
592
695
  return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
593
696
  }
594
697
  // Match constructor by field names — prefer exact match over first-field heuristic
@@ -792,9 +895,9 @@ function emitDecl(d) {
792
895
  const collides = new Set([...typesByField].filter(([, s]) => s.size > 1).map(([n]) => n));
793
896
  const ctors = d.constructors.map(c => {
794
897
  if (c.fields.length === 0)
795
- return escapeName(c.name);
796
- const fields = c.fields.map(f => collides.has(f.name) ? { ...f, name: `${f.name}_${c.name}` } : f);
797
- return `${escapeName(c.name)}(${paramList(fields)})`;
898
+ return dafnyCtorName(c.name);
899
+ const fields = c.fields.map(f => collides.has(f.name) ? { ...f, name: `${f.name}_${c.name.replace(/[^A-Za-z0-9_'?]/g, "_")}` } : f);
900
+ return `${dafnyCtorName(c.name)}(${paramList(fields)})`;
798
901
  });
799
902
  return `datatype ${escapeName(d.name)}${tp} = ${ctors.join(" | ")}`;
800
903
  }
@@ -808,7 +911,10 @@ function emitDecl(d) {
808
911
  case "opaque-type": {
809
912
  // Abstract type — no definition. `(==)` so it can sit inside datatypes
810
913
  // that derive structural equality. Never constructed or destructured.
811
- return `type ${escapeName(d.name)}(==)`;
914
+ // `0` (auto-init) only when a havoc of this type needs a witness to
915
+ // satisfy definite assignment — `var x: T := *` requires it.
916
+ const autoInit = _havocedTypeNames.has(d.name) ? ", 0" : "";
917
+ return `type ${escapeName(d.name)}(==${autoInit})`;
812
918
  }
813
919
  case "def": {
814
920
  const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
@@ -1036,6 +1142,19 @@ const SEQ_FIND_LAST_INDEX = `function SeqFindLastIndex<T>(s: seq<T>, p: T -> boo
1036
1142
  else if p(s[|s|-1]) then |s| - 1
1037
1143
  else SeqFindLastIndex(s[..|s|-1], p)
1038
1144
  }`;
1145
+ const SEQ_FIND = `function SeqFind<T>(s: seq<T>, p: T -> bool): Option<T>
1146
+ ensures SeqFind(s, p).Some? ==> p(SeqFind(s, p).value)
1147
+ ensures SeqFind(s, p).Some? ==> SeqFind(s, p).value in s
1148
+ ensures SeqFind(s, p).Some? ==>
1149
+ exists i: nat :: i < |s| && s[i] == SeqFind(s, p).value && p(s[i]) &&
1150
+ (forall j: nat :: j < i ==> !p(s[j]))
1151
+ ensures SeqFind(s, p).None? ==> forall i :: 0 <= i < |s| ==> !p(s[i])
1152
+ decreases |s|
1153
+ {
1154
+ if |s| == 0 then None
1155
+ else if p(s[0]) then Some(s[0])
1156
+ else SeqFind(s[1..], p)
1157
+ }`;
1039
1158
  const SEQ_FIND_LAST = `function SeqFindLast<T>(s: seq<T>, p: T -> bool): Option<T>
1040
1159
  ensures SeqFindLast(s, p).Some? ==> p(SeqFindLast(s, p).value)
1041
1160
  ensures SeqFindLast(s, p).Some? ==> SeqFindLast(s, p).value in s
@@ -1189,6 +1308,27 @@ const STRING_TO_UPPER = `function StringToUpper(s: string): string
1189
1308
  var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
1190
1309
  [upper] + StringToUpper(s[1..])
1191
1310
  }`;
1311
+ // `String.fromCharCode(n)` — the inverse of `s.charCodeAt(i)`'s `(s[i] as int)`.
1312
+ // Dafny's `char` is a Unicode scalar value, so the argument must miss the
1313
+ // surrogate range; that is the `requires`, discharged at each call site. The two
1314
+ // `ensures` give callers the round-trip law without unfolding the body.
1315
+ const STRING_FROM_CHAR_CODE = `function StringFromCharCode(n: int): string
1316
+ requires 0 <= n < 0xD800 || 0xE000 <= n < 0x110000
1317
+ ensures |StringFromCharCode(n)| == 1
1318
+ ensures (StringFromCharCode(n)[0] as int) == n
1319
+ {
1320
+ [n as char]
1321
+ }`;
1322
+ // `s.repeat(n)` — n copies of s, concatenated. The per-index ensures is stated
1323
+ // for the single-character receiver (the common case: padding with one digit).
1324
+ const STRING_REPEAT = `function StringRepeat(s: string, n: int): string
1325
+ requires n >= 0
1326
+ ensures |StringRepeat(s, n)| == |s| * n
1327
+ ensures |s| == 1 ==> forall i :: 0 <= i < n ==> StringRepeat(s, n)[i] == s[0]
1328
+ decreases n
1329
+ {
1330
+ if n == 0 then "" else s + StringRepeat(s, n - 1)
1331
+ }`;
1192
1332
  const MATH_MIN = `function MathMin(a: int, b: int): int { if a <= b then a else b }`;
1193
1333
  const MATH_MAX = `function MathMax(a: int, b: int): int { if a >= b then a else b }`;
1194
1334
  const MAX_OF_SEQ = `function MaxOfSeq(s: seq<int>): int
@@ -1294,6 +1434,7 @@ const PREAMBLE_CODE = [
1294
1434
  ["SeqFindIndex", SEQ_FIND_INDEX],
1295
1435
  ["SeqFindLastIndex", SEQ_FIND_LAST_INDEX],
1296
1436
  ["SeqFilterSome", SEQ_FILTER_SOME],
1437
+ ["SeqFind", SEQ_FIND],
1297
1438
  ["SeqFindLast", SEQ_FIND_LAST],
1298
1439
  ["SeqFlatten", SEQ_FLATTEN],
1299
1440
  ["SeqJoin", SEQ_JOIN],
@@ -1305,6 +1446,8 @@ const PREAMBLE_CODE = [
1305
1446
  ["StringTrim", STRING_TRIM],
1306
1447
  ["StringToLower", STRING_TO_LOWER],
1307
1448
  ["StringToUpper", STRING_TO_UPPER],
1449
+ ["StringFromCharCode", STRING_FROM_CHAR_CODE],
1450
+ ["StringRepeat", STRING_REPEAT],
1308
1451
  ["NatToString", NAT_TO_STRING],
1309
1452
  ["IntToString", INT_TO_STRING],
1310
1453
  ["MathAbs", MATH_ABS],
@@ -1319,10 +1462,18 @@ const PREAMBLE_CODE = [
1319
1462
  let _recordCtors = new Map();
1320
1463
  let _structureDecls = new Map();
1321
1464
  let _declaredTypes = new Set();
1465
+ let _ambiguousCtors = new Set();
1466
+ // `"<union>.<ctor>.<field>"` → per-constructor destructor name, for fields the
1467
+ // inductive emission renames (shared name, differing types). Field reads and
1468
+ // datatype updates with a pinned ctor must use the renamed destructor.
1469
+ let _ctorFieldRenames = new Map();
1322
1470
  function buildRecordCtorMap(decls) {
1323
1471
  _recordCtors = new Map();
1324
1472
  _structureDecls = new Map();
1325
1473
  _declaredTypes = new Set();
1474
+ _ambiguousCtors = new Set();
1475
+ _ctorFieldRenames = new Map();
1476
+ const ctorSeen = new Set();
1326
1477
  function collectDecl(d) {
1327
1478
  if (d.kind === "structure") {
1328
1479
  _declaredTypes.add(d.name);
@@ -1330,8 +1481,36 @@ function buildRecordCtorMap(decls) {
1330
1481
  if (d.fields.length > 0)
1331
1482
  _recordCtors.set(d.fields[0].name, d.name);
1332
1483
  }
1333
- if (d.kind === "inductive")
1484
+ if (d.kind === "inductive") {
1334
1485
  _declaredTypes.add(d.name);
1486
+ // Constructor names shared by two datatypes in this module (Expr.let vs
1487
+ // Stmt.let) can't be used bare — emitters must qualify them.
1488
+ for (const c of d.constructors) {
1489
+ if (ctorSeen.has(c.name))
1490
+ _ambiguousCtors.add(c.name);
1491
+ ctorSeen.add(c.name);
1492
+ }
1493
+ // Mirror the destructor renaming the inductive case of emitDecl performs
1494
+ // (shared field name, differing types → per-constructor names), so reads
1495
+ // and updates can be translated to the renamed destructors.
1496
+ const typesByField = new Map();
1497
+ for (const c of d.constructors)
1498
+ for (const f of c.fields) {
1499
+ let s = typesByField.get(f.name);
1500
+ if (!s) {
1501
+ s = new Set();
1502
+ typesByField.set(f.name, s);
1503
+ }
1504
+ s.add(tyToDafny(f.type));
1505
+ }
1506
+ const collides = new Set([...typesByField].filter(([, s]) => s.size > 1).map(([n]) => n));
1507
+ for (const c of d.constructors)
1508
+ for (const f of c.fields) {
1509
+ if (!collides.has(f.name))
1510
+ continue;
1511
+ _ctorFieldRenames.set(`${d.name}.${c.name}.${f.name}`, `${f.name}_${c.name.replace(/[^A-Za-z0-9_'?]/g, "_")}`);
1512
+ }
1513
+ }
1335
1514
  if (d.kind === "type-alias")
1336
1515
  _declaredTypes.add(d.name);
1337
1516
  if (d.kind === "def")
@@ -1359,9 +1538,19 @@ function resolveTy(ty) {
1359
1538
  return { kind: "set", elem: resolveTy(ty.elem) };
1360
1539
  return ty;
1361
1540
  }
1541
+ /** Constructor names come from source strings (string-union values like
1542
+ * "spec-pure", discriminated-union tags), which may contain characters no
1543
+ * TS identifier has; map those to `_` before the ordinary escaping. A
1544
+ * collision after mapping fails loudly in Dafny (duplicate constructor)
1545
+ * rather than silently merging. */
1546
+ function dafnyCtorName(name) {
1547
+ return escapeName(name.replace(/[^A-Za-z0-9_'?]/g, "_"));
1548
+ }
1362
1549
  function qualifyCtor(name, type) {
1363
1550
  const rawName = name.replace(/^\./, "");
1364
- const mapped = CTOR_MAP[rawName] ?? escapeName(rawName);
1551
+ // hasOwn: a ctor literally named "constructor" (the IR's own Expr variant)
1552
+ // must not hit Object.prototype.constructor through the bare index.
1553
+ const mapped = (Object.hasOwn(CTOR_MAP, rawName) ? CTOR_MAP[rawName] : undefined) ?? dafnyCtorName(rawName);
1365
1554
  if (type)
1366
1555
  return `${type}.${mapped}`;
1367
1556
  return mapped;
@@ -1375,7 +1564,7 @@ const CTOR_MAP = { "some": "Some", "none": "None" };
1375
1564
  function translatePattern(p) {
1376
1565
  if (p.kind === "wild")
1377
1566
  return "_";
1378
- const ctorName = CTOR_MAP[p.ctor] ?? escapeName(p.ctor);
1567
+ const ctorName = (Object.hasOwn(CTOR_MAP, p.ctor) ? CTOR_MAP[p.ctor] : undefined) ?? dafnyCtorName(p.ctor);
1379
1568
  if (p.binders.length === 0)
1380
1569
  return ctorName;
1381
1570
  return `${ctorName}(${p.binders.map(escapeName).join(", ")})`;
@@ -1385,6 +1574,8 @@ export function emitDafnyFile(file, tsFileName, opts) {
1385
1574
  resetDafnyNameCache();
1386
1575
  buildRecordCtorMap(file.decls);
1387
1576
  _neededPreambles.clear();
1577
+ _havocedTypeNames.clear();
1578
+ collectHavocedTypeNames(file.decls, _havocedTypeNames);
1388
1579
  // Track successfully emitted pure defs — method wrappers are only
1389
1580
  // skipped when the corresponding pure def was actually emitted.
1390
1581
  const emittedPureDefs = new Set();