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.
@@ -4,9 +4,13 @@
4
4
  * Consumes resolved types and classifications.
5
5
  * No type lookups, no string parsing, no re-inference.
6
6
  */
7
+ import { tyEqual } from "./typedir.js";
7
8
  import { anyExprInStmts, pWild, pCtor, patternBinders, patternBinds, patternCtor } from "./ir.js";
8
9
  import { parseTsType } from "./types.js";
9
10
  import { freshName } from "./names.js";
11
+ import { builtinSpec } from "./builtins.js";
12
+ import { isFalsyCapableTy } from "./condition-facts.js";
13
+ import { declOf, declOfKind, declOfTy, unionDeclOfTy, declWithVariant, tyBaseName } from "./typedecls.js";
10
14
  // ── Generic IR walkers ──────────────────────────────────────
11
15
  /**
12
16
  * Map over all sub-expressions in an Expr. `f` is called on each node;
@@ -21,6 +25,7 @@ function mapExpr(e, f) {
21
25
  switch (e.kind) {
22
26
  case "var":
23
27
  case "num":
28
+ case "bigint":
24
29
  case "bool":
25
30
  case "str":
26
31
  case "emptyMap":
@@ -43,7 +48,7 @@ function mapExpr(e, f) {
43
48
  case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
44
49
  case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
45
50
  case "match": {
46
- const scr = typeof e.scrutinee === "string" ? e.scrutinee : r(e.scrutinee);
51
+ const scr = r(e.scrutinee);
47
52
  return { ...e, scrutinee: scr, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
48
53
  }
49
54
  case "forall": return { ...e, body: r(e.body) };
@@ -66,7 +71,7 @@ function mapStmt(s, f) {
66
71
  case "continue": return s;
67
72
  case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapStmt(t, f)), else: s.else.map(t => mapStmt(t, f)) };
68
73
  case "match": {
69
- const scr = typeof s.scrutinee === "string" ? s.scrutinee : r(s.scrutinee);
74
+ const scr = r(s.scrutinee);
70
75
  return { ...s, scrutinee: scr, arms: s.arms.map(a => ({ ...a, body: a.body.map(t => mapStmt(t, f)) })) };
71
76
  }
72
77
  case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
@@ -81,7 +86,10 @@ function mapStmt(s, f) {
81
86
  * rest of the block), `match` arm patterns, `forall`/`exists`, and `for-in`
82
87
  * indices. Capture-avoiding: a nested scope that reintroduces `from` keeps its
83
88
  * own binding untouched. `mapExpr` doesn't descend into lambda bodies, so this
84
- * walks them by hand. */
89
+ * walks them by hand — statement binders are tracked only at that body's top
90
+ * level, which is enough for the sole caller (dafny-emit's
91
+ * `comprehensionBinder`). TODO: generalize if more callers need this. */
92
+ const varE = (name) => ({ kind: "var", name });
85
93
  export function renameFreeVar(e, from, to) {
86
94
  const f = (x) => {
87
95
  if (x.kind === "var")
@@ -93,9 +101,7 @@ export function renameFreeVar(e, from, to) {
93
101
  if ((x.kind === "forall" || x.kind === "exists") && x.var === from)
94
102
  return x;
95
103
  if (x.kind === "match") {
96
- const scr = typeof x.scrutinee === "string"
97
- ? (x.scrutinee === from ? to : x.scrutinee) : mapExpr(x.scrutinee, f);
98
- return { ...x, scrutinee: scr, arms: x.arms.map(a => patternBinds(a.pattern, from) ? a : { ...a, body: mapExpr(a.body, f) }) };
104
+ return { ...x, scrutinee: mapExpr(x.scrutinee, f), arms: x.arms.map(a => patternBinds(a.pattern, from) ? a : { ...a, body: mapExpr(a.body, f) }) };
99
105
  }
100
106
  if (x.kind === "lambda") {
101
107
  if (x.params.some(p => p.name === from))
@@ -128,6 +134,7 @@ function mapTExpr(e, f) {
128
134
  switch (e.kind) {
129
135
  case "var":
130
136
  case "num":
137
+ case "bigint":
131
138
  case "str":
132
139
  case "bool":
133
140
  case "havoc": return e;
@@ -193,8 +200,33 @@ let _typeDecls = [];
193
200
  * in a higher-order position resolves to the monadic method, so it must be
194
201
  * redirected to the pure mirror. Set once per module transform. */
195
202
  let _pureDefNames = new Set();
196
- /** Array methods that take a function argument. */
197
- const HOF_METHODS = new Set(["map", "filter", "every", "some", "find", "findLast", "findIndex", "findLastIndex", "reduce"]);
203
+ /** Discriminated unions whose tag is read as a *value* (`x.kind` compared
204
+ * to another union's tag, passed as an argument, …). Each gets a generated
205
+ * `<Union>_<disc>` discriminator function beside its datatype, returning
206
+ * the source tag strings. Narrowing consumes discriminant *checks*, so this
207
+ * fires only for surviving reads. Populated during body transforms; drained
208
+ * into the types file. */
209
+ let _neededKindHelpers = new Map();
210
+ function kindHelperName(decl) {
211
+ return freshName(`${decl.name}_${decl.discriminant}`);
212
+ }
213
+ function kindHelperDecl(decl) {
214
+ return {
215
+ kind: "def",
216
+ name: kindHelperName(decl),
217
+ typeParams: [],
218
+ params: [{ name: "t", type: { kind: "user", name: decl.name } }],
219
+ returnType: { kind: "string" },
220
+ requires: [], ensures: [], decreases: null,
221
+ body: {
222
+ kind: "match", scrutinee: varE("t"),
223
+ arms: decl.variants.map(v => ({
224
+ pattern: { kind: "ctor", ctor: v.name, binders: v.fields.map((_, i) => `_${i}`) },
225
+ body: { kind: "str", value: v.name },
226
+ })),
227
+ },
228
+ };
229
+ }
198
230
  /** Prefix match-bound field names to avoid capturing user variables.
199
231
  * When prefix is given (the scrutinee name), include it to avoid
200
232
  * collisions in nested matches on different variables. `freshName` closes
@@ -214,17 +246,16 @@ function isIntegral(ty) { return ty.kind === "int" || ty.kind === "nat"; }
214
246
  function isArray(ty) { return ty.kind === "array"; }
215
247
  function isUser(ty) { return ty.kind === "user"; }
216
248
  function isRecordType(ty) {
217
- if (ty.kind !== "user")
218
- return false;
219
- const base = ty.name.includes("<") ? ty.name.slice(0, ty.name.indexOf("<")) : ty.name;
220
- return _typeDecls.find(d => d.name === base)?.kind === "record";
249
+ return declOfTy(_typeDecls, ty)?.kind === "record";
221
250
  }
222
251
  /** Truthiness test for a *lowered* value of source type `ty`, used by `||`
223
- * falsiness lowering. Mirrors narrow.ts's `canBeFalsy`: only int/nat/string/bool
224
- * values can be falsy in JS (`0`, `""`, `false`); every other value (array, user
225
- * type, …) is always truthy. Returns null for the always-truthy types so callers
226
- * can unwrap directly instead of emitting a redundant guard. */
252
+ * falsiness lowering. Which types need one is `isFalsyCapableTy` (shared with
253
+ * condition-facts' falsy gate); this adds the per-type test. Returns null for
254
+ * the always-truthy types so callers can unwrap directly instead of emitting
255
+ * a redundant guard. */
227
256
  function valueTruthyCond(value, ty) {
257
+ if (!isFalsyCapableTy(ty))
258
+ return null;
228
259
  switch (ty.kind) {
229
260
  case "int":
230
261
  case "nat":
@@ -350,7 +381,7 @@ function wrapOptionalBranch(expr, raw) {
350
381
  // like the scrutinee of an outer match. Dafny treats `Option.Some` and bare
351
382
  // `Some` equivalently — the qualification is harmless there.
352
383
  if (raw.kind === "var" && raw.name === "undefined")
353
- return { kind: "constructor", name: "none", type: "Option" };
384
+ return { kind: "constructor", name: "none", type: "Option", args: [] };
354
385
  if (raw.ty.kind === "optional")
355
386
  return expr; // already Option<T>, don't double-wrap
356
387
  return { kind: "constructor", name: "some", type: "Option", args: [expr] };
@@ -419,12 +450,16 @@ function lowerExpr(e, binds) {
419
450
  switch (e.kind) {
420
451
  case "var": return { kind: "var", name: e.name };
421
452
  case "num": return { kind: "num", value: e.value };
453
+ case "bigint": return { kind: "bigint", value: e.value };
422
454
  case "bool": return { kind: "bool", value: e.value };
423
455
  case "str":
424
456
  if (e.ty.kind === "user")
425
- return { kind: "constructor", name: e.value, type: e.ty.name };
457
+ return { kind: "constructor", name: e.value, type: e.ty.name, args: [] };
426
458
  return { kind: "str", value: e.value };
427
459
  case "unop":
460
+ // Only `num` folds. A `bigint` payload is a string, so negating it here
461
+ // would coerce through `Number` and round: `-9007199254740993n` stays a
462
+ // structural `unop("-", bigint(...))` and is negated by the emitter.
428
463
  if (e.op === "-" && e.expr.kind === "num")
429
464
  return { kind: "num", value: -e.expr.value };
430
465
  // String truthiness: !str → str == ""
@@ -469,7 +504,7 @@ function lowerExpr(e, binds) {
469
504
  kind: "binop",
470
505
  op: e.op === "===" ? "=" : "≠",
471
506
  left: transformExpr(e.left.obj),
472
- right: { kind: "constructor", name: e.right.value, type: objTy },
507
+ right: { kind: "constructor", name: e.right.value, type: objTy, args: [] },
473
508
  };
474
509
  }
475
510
  // String literal comparison — constructor if user type, string literal if string.
@@ -479,7 +514,7 @@ function lowerExpr(e, binds) {
479
514
  const left = lowerExpr(e.left, binds);
480
515
  const leftTy = e.left.ty.kind === "user" ? e.left.ty.name : undefined;
481
516
  const right = isUser(e.left.ty)
482
- ? { kind: "constructor", name: e.right.value, type: leftTy }
517
+ ? { kind: "constructor", name: e.right.value, type: leftTy, args: [] }
483
518
  : { kind: "str", value: e.right.value };
484
519
  return { kind: "binop", op: e.op === "===" ? "=" : "≠", left, right };
485
520
  }
@@ -504,7 +539,7 @@ function lowerExpr(e, binds) {
504
539
  // non-optional string-literal rule above.
505
540
  const innerTy = optSide.ty.kind === "optional" ? optSide.ty.inner : optSide.ty;
506
541
  const valExpr = valSide.kind === "str" && innerTy.kind === "user"
507
- ? { kind: "constructor", name: valSide.value, type: innerTy.name }
542
+ ? { kind: "constructor", name: valSide.value, type: innerTy.name, args: [] }
508
543
  : lowerExpr(valSide, binds);
509
544
  const cmpOp = BOOL_OP_MAP[e.op] ?? e.op;
510
545
  const noneVal = e.op === "!==" ? true : false;
@@ -543,7 +578,7 @@ function lowerExpr(e, binds) {
543
578
  // || on optional → match Some/None with default. JS `||` tests falsiness of
544
579
  // the *unwrapped* value, so when the inner type can be falsy the Some arm must
545
580
  // re-test (`Some(0) || 1 === 1`); array/user inners are always truthy and
546
- // unwrap directly. Mirrors narrow.ts's canBeFalsy gate.
581
+ // unwrap directly. Same gate as condition-facts' `canBeFalsy`.
547
582
  if (e.op === "||" && e.left.ty.kind === "optional") {
548
583
  const optExpr = lowerExpr(e.left, binds);
549
584
  const defaultExpr = lowerExpr(e.right, binds);
@@ -694,21 +729,41 @@ function lowerExpr(e, binds) {
694
729
  // as bare truthiness checks. Emit as the Dafny discriminator predicate for the
695
730
  // 'true' variant: result.ok → result.true_?
696
731
  if (e.isDiscriminant && e.obj.ty.kind === "user") {
697
- const baseName = e.obj.ty.name.includes("<") ? e.obj.ty.name.slice(0, e.obj.ty.name.indexOf("<")) : e.obj.ty.name;
698
- const decl = _typeDecls.find(d => d.name === baseName && d.kind === "discriminated-union");
732
+ const decl = unionDeclOfTy(_typeDecls, e.obj.ty);
699
733
  if (decl?.variants?.some(v => v.name === "true")) {
700
734
  return { kind: "field", obj: transformExpr(e.obj), field: "true_?" };
701
735
  }
736
+ // Surviving string-discriminant read (`x.kind` used as a value —
737
+ // compared against another union's tag, passed as an argument, …):
738
+ // lower to the generated per-union discriminator function, which
739
+ // returns the source tag strings. Narrowing consumes discriminant
740
+ // *checks*; this catches reads that survive as values.
741
+ if (decl?.variants && decl.discriminant && decl.discriminant !== "__isArray__") {
742
+ _neededKindHelpers.set(decl.name, decl);
743
+ return { kind: "app", fn: kindHelperName(decl), args: [lowerExpr(e.obj, binds)] };
744
+ }
702
745
  }
703
746
  // Union destructor: `x.field` where x is a discriminated union and `field`
704
747
  // is a data field of one of its variants. Dafny reads the destructor
705
748
  // directly; Lean has no field projection on a multi-ctor inductive, so tag
706
749
  // the node with the union's base name and let the Lean emitter `match`.
707
750
  if (e.obj.ty.kind === "user") {
708
- const baseName = e.obj.ty.name.includes("<") ? e.obj.ty.name.slice(0, e.obj.ty.name.indexOf("<")) : e.obj.ty.name;
709
- const decl = _typeDecls.find(d => d.name === baseName && d.kind === "discriminated-union");
710
- if (decl?.variants?.some(v => v.fields.some(f => f.name === e.field))) {
711
- return { kind: "field", obj: transformExpr(e.obj), field: e.field, fromUnion: baseName, datatypeField: true };
751
+ const baseName = tyBaseName(e.obj.ty.name);
752
+ const decl = declOfKind(_typeDecls, baseName, "discriminated-union");
753
+ const owners = decl?.variants?.filter(v => v.fields.some(f => f.name === e.field)) ?? [];
754
+ if (owners.length > 0) {
755
+ // Shared field name with differing declared types: those destructors
756
+ // are renamed per-constructor, and the read's own resolved type
757
+ // identifies the owning variant (the types differ exactly when the
758
+ // rename happens). Pin it so emitters use the renamed destructor.
759
+ const ownerTy = (v) => v.fields.find(f => f.name === e.field)?.type;
760
+ const differ = owners.some(v => {
761
+ const a = ownerTy(v), b = ownerTy(owners[0]);
762
+ return a && b && !tyEqual(a, b);
763
+ });
764
+ const matching = differ ? owners.filter(v => { const t = ownerTy(v); return t && tyEqual(t, e.ty); }) : [];
765
+ const ctor = e.ofVariant ?? (matching.length === 1 ? matching[0].name : undefined);
766
+ return { kind: "field", obj: transformExpr(e.obj), field: e.field, fromUnion: baseName, ctor, datatypeField: true };
712
767
  }
713
768
  }
714
769
  return { kind: "field", obj: transformExpr(e.obj), field: e.field, datatypeField: isRecordType(e.obj.ty) };
@@ -737,13 +792,12 @@ function lowerExpr(e, binds) {
737
792
  e.fn.field === "isArray" && e.args.length === 1) {
738
793
  const arg = e.args[0];
739
794
  if (arg.ty.kind === "user") {
740
- const baseName = arg.ty.name.includes("<") ? arg.ty.name.slice(0, arg.ty.name.indexOf("<")) : arg.ty.name;
741
- const decl = _typeDecls.find(d => d.name === baseName);
795
+ const decl = declOfTy(_typeDecls, arg.ty);
742
796
  if (decl?.kind === "discriminated-union" && decl.discriminant === "__isArray__") {
743
797
  return {
744
798
  kind: "binop", op: "=",
745
799
  left: lowerExpr(arg, binds),
746
- right: { kind: "constructor", name: "ArrayBranch", type: arg.ty.name },
800
+ right: { kind: "constructor", name: "ArrayBranch", type: arg.ty.name, args: [] },
747
801
  };
748
802
  }
749
803
  }
@@ -787,7 +841,10 @@ function lowerExpr(e, binds) {
787
841
  if (e.fn.kind === "field") {
788
842
  const recv = lowerExpr(e.fn.obj, binds);
789
843
  let method = e.fn.field;
790
- const isHOF = e.fn.obj.ty.kind === "array" && HOF_METHODS.has(method);
844
+ const spec = e.builtinId !== undefined ? builtinSpec(e.builtinId) : null;
845
+ // Lambda-taking array builtins (registry `hof`, comparator excluded —
846
+ // mirrors the historical HOF_METHODS set).
847
+ const isHOF = spec?.hof !== undefined && spec.hof.shape !== "comparator";
791
848
  const args = e.args.map((a, i) => {
792
849
  const lowered = lowerExpr(a, binds);
793
850
  // Lean: a pure fn passed to a HOF by name resolves to the monadic
@@ -796,9 +853,9 @@ function lowerExpr(e, binds) {
796
853
  lowered.kind === "var" && _pureDefNames.has(lowered.name)) {
797
854
  return { kind: "var", name: `Pure.${lowered.name}` };
798
855
  }
799
- // Array index args must be nat in Lean: `with`'s index (0), includes/indexOf `from` (1).
800
- const isArrIdxArg = e.fn.kind === "field" && e.fn.obj.ty.kind === "array" &&
801
- ((e.fn.field === "with" && i === 0) || ((e.fn.field === "includes" || e.fn.field === "indexOf") && i === 1));
856
+ // Array index args must be nat in Lean: `with`'s index (0), includes/indexOf `from` (1)
857
+ // registry `intArgPositions`.
858
+ const isArrIdxArg = spec?.intArgPositions !== undefined && spec.intArgPositions.includes(i);
802
859
  if (isArrIdxArg && !isNat(a.ty))
803
860
  return { kind: "toNat", expr: lowered };
804
861
  return lowered;
@@ -846,8 +903,8 @@ function lowerExpr(e, binds) {
846
903
  if (e.ty.kind === "user" && !e.spread) {
847
904
  const tyName = e.ty.name;
848
905
  // Match base type name (strip generic args: "Result<Model, Err>" → "Result")
849
- const baseName = tyName.includes("<") ? tyName.slice(0, tyName.indexOf("<")) : tyName;
850
- const decl = _typeDecls.find(d => d.name === baseName && (d.kind === "discriminated-union" || d.kind === "string-union"));
906
+ const baseName = tyBaseName(tyName);
907
+ const decl = declOfKind(_typeDecls, baseName, "discriminated-union", "string-union");
851
908
  if (decl && decl.discriminant) {
852
909
  const discField = e.fields.find(f => f.name === decl.discriminant);
853
910
  if (discField && (discField.value.kind === "str" || discField.value.kind === "bool")) {
@@ -855,8 +912,11 @@ function lowerExpr(e, binds) {
855
912
  const variant = decl.variants?.find(v => v.name === variantName);
856
913
  if (variant) {
857
914
  const nonDiscFields = e.fields.filter(f => f.name !== decl.discriminant);
858
- if (nonDiscFields.length === 0) {
859
- return { kind: "constructor", name: variantName, type: tyName };
915
+ // Bare-constructor shortcut only when the variant truly has no
916
+ // fields a variant with only optional fields still needs its
917
+ // None-filled argument list (`int_(None)`, not `int_`).
918
+ if (variant.fields.length === 0) {
919
+ return { kind: "constructor", name: variantName, type: tyName, args: [] };
860
920
  }
861
921
  // Constructor with args: match variant field order. Emit a bare `app`
862
922
  // (Dafny renders `variantName(args)`, a valid unqualified constructor
@@ -877,9 +937,9 @@ function lowerExpr(e, binds) {
877
937
  if (e.spread) {
878
938
  const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
879
939
  const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
880
- const structDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "record") : undefined;
940
+ const structDecl = structName ? declOfKind(_typeDecls, structName, "record") : undefined;
881
941
  // Also check discriminated-union variants for field types
882
- const unionDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "discriminated-union") : undefined;
942
+ const unionDecl = structName ? declOfKind(_typeDecls, structName, "discriminated-union") : undefined;
883
943
  const loweredFields = e.fields.map(f => {
884
944
  // Propagate declared field type onto value if it has unknown type
885
945
  let fieldValue = f.value;
@@ -932,10 +992,8 @@ function lowerExpr(e, binds) {
932
992
  // Carry the resolved record type so the emitter can pick the right
933
993
  // constructor when two datatypes share a field-name set (Event vs
934
994
  // SparseEvent) — structural matching alone would take the first-declared.
935
- const recName = e.ty.kind === "user"
936
- ? (e.ty.name.includes("<") ? e.ty.name.slice(0, e.ty.name.indexOf("<")) : e.ty.name)
937
- : undefined;
938
- const ctor = recName && _typeDecls.find(d => d.name === recName && d.kind === "record") ? recName : undefined;
995
+ const recName = e.ty.kind === "user" ? tyBaseName(e.ty.name) : undefined;
996
+ const ctor = recName && declOfKind(_typeDecls, recName, "record") ? recName : undefined;
939
997
  return { kind: "record", spread: null, ctor, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
940
998
  }
941
999
  case "arrayLiteral":
@@ -985,10 +1043,10 @@ function lowerExpr(e, binds) {
985
1043
  }
986
1044
  case "optChain":
987
1045
  // Narrow should have rewritten optChain to someMatch.
988
- throw new Error(`optChain reached transform — narrow should have rewritten it`);
1046
+ throw new Error(`optChain reached transform — narrow should have rewritten it: ${JSON.stringify(e).slice(0, 400)}`);
989
1047
  case "nullish":
990
1048
  // Narrow should have rewritten nullish to someMatch.
991
- throw new Error(`nullish reached transform — narrow should have rewritten it`);
1049
+ throw new Error(`nullish reached transform — narrow should have rewritten it: ${JSON.stringify(e).slice(0, 300)}`);
992
1050
  case "havoc":
993
1051
  // Dafny's * only works in var/assign positions — lift to own declaration
994
1052
  if (binds) {
@@ -1009,7 +1067,7 @@ function lowerExpr(e, binds) {
1009
1067
  // Bare-var shortcut, but route \result through lowerExpr so the
1010
1068
  // lemma-side replaceVar pass can substitute it with the function call.
1011
1069
  scrutinee = path.fields.length === 0 && path.rootVar !== "\\result"
1012
- ? path.rootVar
1070
+ ? varE(path.rootVar)
1013
1071
  : lowerExpr(e.scrutinee, binds);
1014
1072
  }
1015
1073
  else {
@@ -1040,7 +1098,7 @@ function lowerExpr(e, binds) {
1040
1098
  // Path scrutinees (e.g. `m.content`) get a synthesized hint derived
1041
1099
  // from the last field/var name so the binder reads naturally.
1042
1100
  const scrutinee = lowerExpr(e.scrutinee, binds);
1043
- const decl = _typeDecls.find(d => d.name === e.typeName);
1101
+ const decl = declOf(_typeDecls, e.typeName);
1044
1102
  const isSynthArrayUnion = decl?.discriminant === "__isArray__";
1045
1103
  const varName = e.scrutinee.kind === "var" ? e.scrutinee.name : undefined;
1046
1104
  const pathHint = varName ?? scrutineeHint(e.scrutinee);
@@ -1050,7 +1108,7 @@ function lowerExpr(e, binds) {
1050
1108
  const fields = variant?.fields ?? [];
1051
1109
  let body = lowerExpr(c.body, binds);
1052
1110
  if (varName && fields.length > 0) {
1053
- body = replaceFieldAccess(body, varName, fields);
1111
+ body = replaceFieldAccess(body, varName, fields, c.variant, tyBaseName(e.typeName));
1054
1112
  if (isSynthArrayUnion && fields.length === 1) {
1055
1113
  body = replaceVarInExpr(body, varName, matchBinder(fields[0].name, varName));
1056
1114
  }
@@ -1071,7 +1129,7 @@ function lowerExpr(e, binds) {
1071
1129
  body = wrapOptionalBranch(body, e.fallthrough);
1072
1130
  arms.push({ pattern: pWild(), body });
1073
1131
  }
1074
- return { kind: "match", scrutinee: varName ?? scrutinee, arms };
1132
+ return { kind: "match", scrutinee: varName !== undefined ? varE(varName) : scrutinee, arms };
1075
1133
  }
1076
1134
  }
1077
1135
  }
@@ -1100,7 +1158,7 @@ function ensuresToMatch(e, typeDecls) {
1100
1158
  if (obj.kind !== "var" || obj.ty.kind !== "user")
1101
1159
  return null;
1102
1160
  const typeName = obj.ty.name;
1103
- const decl = typeDecls.find(d => d.name === typeName && d.kind === "discriminated-union");
1161
+ const decl = declOfKind(typeDecls, typeName, "discriminated-union");
1104
1162
  if (!decl)
1105
1163
  return null;
1106
1164
  const variantName = e.left.right.value;
@@ -1111,18 +1169,26 @@ function ensuresToMatch(e, typeDecls) {
1111
1169
  const pattern = buildMatchPattern(variantName, fields, obj.name);
1112
1170
  let rhs = transformExpr(e.right);
1113
1171
  rhs = replaceFieldAccess(rhs, obj.name, fields);
1114
- return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: pWild(), body: { kind: "bool", value: true } }] };
1172
+ return { kind: "match", scrutinee: varE(obj.name), arms: [{ pattern, body: rhs }, { pattern: pWild(), body: { kind: "bool", value: true } }] };
1115
1173
  }
1116
- function replaceFieldAccess(e, varName, fields) {
1174
+ function replaceFieldAccess(e, varName, fields, ctorName, ctorOf) {
1117
1175
  return mapExpr(e, x => {
1118
1176
  if (x.kind === "field" && x.obj.kind === "var" && x.obj.name === varName) {
1119
1177
  const f = fields.find(f => f.name === x.field);
1120
1178
  if (f)
1121
1179
  return { kind: "var", name: matchBinder(f.name, varName) };
1122
1180
  }
1181
+ // Datatype update of the scrutinee (`{ ...vn, f: v }`): the arm knows the
1182
+ // variant, so stamp it — emitters need it for per-constructor destructor
1183
+ // names. Recurse manually (returning a node stops mapExpr's own descent).
1184
+ if (ctorName && x.kind === "record" && !x.ctor && x.spread &&
1185
+ x.spread.kind === "var" && x.spread.name === varName) {
1186
+ return { ...x, ctor: ctorName, ctorOf,
1187
+ fields: x.fields.map(f => ({ ...f, value: replaceFieldAccess(f.value, varName, fields, ctorName, ctorOf) })) };
1188
+ }
1123
1189
  // If this let shadows the matched variable, stop replacing in the body
1124
1190
  if (x.kind === "let" && x.name === varName)
1125
- return { ...x, value: replaceFieldAccess(x.value, varName, fields) };
1191
+ return { ...x, value: replaceFieldAccess(x.value, varName, fields, ctorName, ctorOf) };
1126
1192
  return null;
1127
1193
  });
1128
1194
  }
@@ -1176,11 +1242,12 @@ function scrutineeHint(e) {
1176
1242
  // `if (X) continue; rest` → `if (!X) { rest }` at the top of a loop body.
1177
1243
  // Dafny's lowered while-loops have the index increment at the bottom, so a
1178
1244
  // `continue` would skip it and loop forever; rewriting to if/else lets the
1179
- // loop fall through normally.
1245
+ // loop fall through normally. The operand is already-lowered IR, where
1246
+ // negation is spelled `¬` (lowerExpr rewrites `!`).
1180
1247
  function negateExpr(e) {
1181
- if (e.kind === "unop" && e.op === "!")
1248
+ if (e.kind === "unop" && e.op === "¬")
1182
1249
  return e.expr;
1183
- return { kind: "unop", op: "!", expr: e };
1250
+ return { kind: "unop", op: "¬", expr: e };
1184
1251
  }
1185
1252
  /** Build the two pieces of an `arr.pop()` lowering on a named array variable:
1186
1253
  * - `optValue` is `(if |arr|>0 then Some(arr[|arr|-1]) else None)` (the popped element)
@@ -1300,13 +1367,10 @@ function matchToIfChains(stmts) {
1300
1367
  const arms = s.arms.map(a => ({ ...a, body: matchToIfChains(a.body) }));
1301
1368
  const ctorArms = arms.filter(a => a.pattern.kind !== "wild");
1302
1369
  const firstCtor = ctorArms[0] ? patternCtor(ctorArms[0].pattern) : undefined;
1303
- const decl = firstCtor
1304
- ? _typeDecls.find(d => (d.kind === "discriminated-union" || d.kind === "string-union") &&
1305
- ((d.variants?.some(v => v.name === firstCtor)) || (d.values?.includes(firstCtor))))
1306
- : undefined;
1370
+ const decl = firstCtor ? declWithVariant(_typeDecls, firstCtor) : undefined;
1307
1371
  if (!decl)
1308
1372
  return [{ ...s, arms }]; // not a user union (e.g. Option) — leave as match
1309
- const scrutExpr = typeof s.scrutinee === "string" ? { kind: "var", name: s.scrutinee } : s.scrutinee;
1373
+ const scrutExpr = s.scrutinee;
1310
1374
  const defaultArm = arms.find(a => a.pattern.kind === "wild");
1311
1375
  let elseBranch = defaultArm ? defaultArm.body : [];
1312
1376
  for (let k = ctorArms.length - 1; k >= 0; k--) {
@@ -1325,7 +1389,7 @@ function matchToIfChains(stmts) {
1325
1389
  { pattern: pCtor(ctor), body: { kind: "bool", value: true } },
1326
1390
  { pattern: pWild(), body: { kind: "bool", value: false } }
1327
1391
  ] }
1328
- : { kind: "binop", op: "=", left: scrutExpr, right: { kind: "constructor", name: ctor, type: decl.name } };
1392
+ : { kind: "binop", op: "=", left: scrutExpr, right: { kind: "constructor", name: ctor, type: decl.name, args: [] } };
1329
1393
  // Bind only the constructor-field binders the body actually uses, pinning the
1330
1394
  // owning ctor so the destructor doesn't guess (variants share field names).
1331
1395
  const lets = [];
@@ -1381,6 +1445,34 @@ function requireDoneWithForBreaks(stmts, fnName) {
1381
1445
  }
1382
1446
  }
1383
1447
  }
1448
+ /** The forin emission places the index increment at the loop-body end, so a
1449
+ * surviving `continue` would skip it. Insert the increment immediately
1450
+ * before every same-scope continue (nested loops own their continues),
1451
+ * mirroring the C-style-for desugar's discipline. */
1452
+ function insertIncrementBeforeContinue(stmts, idxName) {
1453
+ const incr = { kind: "assign", target: idxName,
1454
+ value: { kind: "binop", op: "+", left: { kind: "var", name: idxName }, right: { kind: "num", value: 1 } } };
1455
+ const walk = (ss) => {
1456
+ const out = [];
1457
+ for (const s of ss) {
1458
+ if (s.kind === "continue") {
1459
+ out.push(incr, s);
1460
+ continue;
1461
+ }
1462
+ if (s.kind === "if") {
1463
+ out.push({ ...s, then: walk(s.then), else: walk(s.else) });
1464
+ continue;
1465
+ }
1466
+ if (s.kind === "match") {
1467
+ out.push({ ...s, arms: s.arms.map(a => ({ ...a, body: walk(a.body) })) });
1468
+ continue;
1469
+ }
1470
+ out.push(s);
1471
+ }
1472
+ return out;
1473
+ };
1474
+ return walk(stmts);
1475
+ }
1384
1476
  function eliminateTopLevelContinue(stmts) {
1385
1477
  const out = [];
1386
1478
  for (let i = 0; i < stmts.length; i++) {
@@ -1489,7 +1581,7 @@ function transformStmts(stmts, typeDecls) {
1489
1581
  const arrSize = { kind: "field", obj: seq, field: "size" };
1490
1582
  // Auto-add bound invariant: idx ≤ bound (always true for range loops)
1491
1583
  const boundInv = { kind: "binop", op: "≤", left: idxVar, right: arrSize };
1492
- const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
1584
+ const bodyStmts = insertIncrementBeforeContinue(eliminateTopLevelContinue(transformStmts(s.body, typeDecls)), idxName);
1493
1585
  result.push({
1494
1586
  kind: "forin", idx: idxName, bound: arrSize,
1495
1587
  invariants: [boundInv, ...s.invariants.map(transformExpr)],
@@ -1692,7 +1784,7 @@ function transformStmt(s, typeDecls) {
1692
1784
  const replaced = replacePathInTStmts(s.someBody, path, s.binder, s.binderTy);
1693
1785
  const someBody = transformStmts(replaced, typeDecls);
1694
1786
  const noneBody = transformStmts(s.noneBody, typeDecls);
1695
- const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
1787
+ const scrutinee = path.fields.length === 0 ? varE(path.rootVar) : transformExpr(s.scrutinee);
1696
1788
  return [{
1697
1789
  kind: "match", scrutinee,
1698
1790
  arms: [
@@ -1716,13 +1808,13 @@ function mapStmtExprs(s, r) {
1716
1808
  * and delegates body transformation to the caller-provided function.
1717
1809
  * Returns null if any body transformation returns null (pure path abort). */
1718
1810
  function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
1719
- const decl = typeName ? typeDecls.find(d => d.name === typeName) : undefined;
1811
+ const decl = typeName ? declOf(typeDecls, typeName) : undefined;
1720
1812
  const arms = [];
1721
1813
  for (const c of cases) {
1722
1814
  const variant = decl?.variants?.find(v => v.name === c.name);
1723
1815
  const fields = variant?.fields ?? [];
1724
1816
  const pattern = buildMatchPattern(c.name, fields, varName);
1725
- const body = transformBody(c.body, varName, fields);
1817
+ const body = transformBody(c.body, varName, fields, c.name);
1726
1818
  if (body === null)
1727
1819
  return null;
1728
1820
  arms.push({ pattern, body });
@@ -1730,7 +1822,7 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
1730
1822
  return arms;
1731
1823
  }
1732
1824
  function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
1733
- const decl = typeDecls.find(d => d.name === typeName);
1825
+ const decl = declOf(typeDecls, typeName);
1734
1826
  // Synth array-unions (discriminant "__isArray__") have single-field variants
1735
1827
  // ArrayBranch(arr) / NonArrayBranch(val). The matched arm refers to the
1736
1828
  // scrutinee by its bare name/path (`content`, `m.content`), not `.arr`, so
@@ -1784,7 +1876,7 @@ function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
1784
1876
  arms.push({ pattern: pWild(), body: transformStmts(fallthrough, typeDecls) });
1785
1877
  }
1786
1878
  }
1787
- return { kind: "match", scrutinee: isPath ? transformExpr(scrutinee) : prefix, arms };
1879
+ return { kind: "match", scrutinee: isPath ? transformExpr(scrutinee) : varE(prefix), arms };
1788
1880
  }
1789
1881
  /** Replace bare `var(oldName)` references → `var(newName)` with the given type.
1790
1882
  * Used by emitMatchStmt for synth array-unions where the variant has a single
@@ -1799,7 +1891,7 @@ function replaceVarInTStmts(stmts, oldName, newName, newTy) {
1799
1891
  }
1800
1892
  /** If the chain has matched all variants but one, return that remaining variant. */
1801
1893
  function remainingVariant(typeName, cases, typeDecls) {
1802
- const decl = typeDecls.find(d => d.name === typeName);
1894
+ const decl = declOf(typeDecls, typeName);
1803
1895
  if (!decl?.variants)
1804
1896
  return null;
1805
1897
  const matched = new Set(cases.map(c => c.variant));
@@ -1818,10 +1910,7 @@ function remainingVariant(typeName, cases, typeDecls) {
1818
1910
  function enumFieldSwitch(s, typeDecls) {
1819
1911
  if (!s.discriminant)
1820
1912
  return null;
1821
- const objBase = s.expr.ty.kind === "user"
1822
- ? (s.expr.ty.name.includes("<") ? s.expr.ty.name.slice(0, s.expr.ty.name.indexOf("<")) : s.expr.ty.name)
1823
- : undefined;
1824
- const objDecl = objBase ? typeDecls.find(d => d.name === objBase) : undefined;
1913
+ const objDecl = declOfTy(typeDecls, s.expr.ty);
1825
1914
  if (objDecl?.kind === "discriminated-union" && objDecl.discriminant === s.discriminant)
1826
1915
  return null;
1827
1916
  const fieldTy = objDecl?.kind === "record" ? objDecl.fields?.find(f => f.name === s.discriminant)?.type : undefined;
@@ -1830,15 +1919,36 @@ function enumFieldSwitch(s, typeDecls) {
1830
1919
  enumTyName: fieldTy?.kind === "user" ? fieldTy.name : undefined,
1831
1920
  };
1832
1921
  }
1922
+ /** Stamp variant ctor info onto datatype updates of the match scrutinee in
1923
+ * lowered arm bodies (`{ ...vn, f: v }`) — the statement-path twin of
1924
+ * `replaceFieldAccess`'s stamping. Emitters need the pin to use
1925
+ * per-constructor destructor names for collision-renamed fields. */
1926
+ function stampScrutineeUpdates(body, varName, ctorName, ctorOf) {
1927
+ const stamp = (x) => {
1928
+ if (x.kind === "record" && !x.ctor && x.spread &&
1929
+ x.spread.kind === "var" && x.spread.name === varName) {
1930
+ return { ...x, ctor: ctorName, ctorOf,
1931
+ fields: x.fields.map(f => ({ ...f, value: mapExpr(f.value, stamp) })) };
1932
+ }
1933
+ return null;
1934
+ };
1935
+ return body.map(st => mapStmt(st, stamp));
1936
+ }
1833
1937
  function emitSwitchStmt(s, typeDecls) {
1834
1938
  const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
1835
1939
  const ef = enumFieldSwitch(s, typeDecls);
1940
+ const baseName = s.expr.ty.kind === "user" ? tyBaseName(s.expr.ty.name) : undefined;
1836
1941
  const arms = ef
1837
1942
  ? buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformStmts(body, typeDecls))
1838
- : buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", s.expr.ty.kind === "user" ? s.expr.ty.name : undefined, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
1943
+ : buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", s.expr.ty.kind === "user" ? s.expr.ty.name : undefined, typeDecls, (body, vn, fields, ctorName) => {
1944
+ let out = transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls);
1945
+ if (ctorName && vn && baseName)
1946
+ out = stampScrutineeUpdates(out, vn, ctorName, baseName);
1947
+ return out;
1948
+ });
1839
1949
  if (s.defaultBody.length > 0)
1840
1950
  arms.push({ pattern: pWild(), body: transformStmts(s.defaultBody, typeDecls) });
1841
- return { kind: "match", scrutinee: ef ? ef.scrutinee : (s.expr.kind === "var" ? s.expr.name : "?"), arms };
1951
+ return { kind: "match", scrutinee: ef ? ef.scrutinee : varE(s.expr.kind === "var" ? s.expr.name : "?"), arms };
1842
1952
  }
1843
1953
  /** Replace obj.field → replacement var in typed IR.
1844
1954
  * Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
@@ -1965,7 +2075,7 @@ function transformPureBody(stmts, typeDecls) {
1965
2075
  const noneExpr = transformPureBody([...s.noneBody, ...rest], typeDecls);
1966
2076
  if (!noneExpr)
1967
2077
  return null;
1968
- const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
2078
+ const scrutinee = path.fields.length === 0 ? varE(path.rootVar) : transformExpr(s.scrutinee);
1969
2079
  return {
1970
2080
  kind: "match", scrutinee,
1971
2081
  arms: [
@@ -1997,16 +2107,16 @@ function transformPureSwitch(s, typeDecls) {
1997
2107
  return { kind: "match", scrutinee: ef.scrutinee, arms };
1998
2108
  }
1999
2109
  const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
2000
- if (!typeDecls.find(d => d.name === typeName))
2110
+ if (!declOf(typeDecls, typeName))
2001
2111
  return null;
2002
2112
  const varName = s.expr.kind === "var" ? s.expr.name : undefined;
2003
2113
  const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
2004
- const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => {
2114
+ const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields, ctorName) => {
2005
2115
  let result = transformPureBody(body, typeDecls);
2006
2116
  if (!result)
2007
2117
  return null;
2008
2118
  if (fields.length > 0 && vn)
2009
- result = replaceFieldAccess(result, vn, fields);
2119
+ result = replaceFieldAccess(result, vn, fields, ctorName, tyBaseName(typeName));
2010
2120
  return result;
2011
2121
  });
2012
2122
  if (!arms)
@@ -2019,21 +2129,21 @@ function transformPureSwitch(s, typeDecls) {
2019
2129
  }
2020
2130
  if (s.expr.kind !== "var")
2021
2131
  return null;
2022
- return { kind: "match", scrutinee: s.expr.name, arms };
2132
+ return { kind: "match", scrutinee: varE(s.expr.name), arms };
2023
2133
  }
2024
2134
  function transformPureMatch(chain, typeDecls) {
2025
2135
  const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
2026
- const decl = typeDecls.find(d => d.name === chain.typeName);
2136
+ const decl = declOf(typeDecls, chain.typeName);
2027
2137
  // Synth array-unions have single-field variants and user code refers to the
2028
2138
  // scrutinee by its bare name, not field-accessed. See emitMatchStmt for
2029
2139
  // the statement-level counterpart of this substitution.
2030
2140
  const isSynthArrayUnion = decl?.discriminant === "__isArray__";
2031
- const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => {
2141
+ const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields, ctorName) => {
2032
2142
  let result = transformPureBody(body, typeDecls);
2033
2143
  if (!result)
2034
2144
  return null;
2035
2145
  if (fields.length > 0 && vn)
2036
- result = replaceFieldAccess(result, vn, fields);
2146
+ result = replaceFieldAccess(result, vn, fields, ctorName, tyBaseName(chain.typeName));
2037
2147
  if (isSynthArrayUnion && fields.length === 1 && vn) {
2038
2148
  result = replaceVarInExpr(result, vn, matchBinder(fields[0].name, vn));
2039
2149
  }
@@ -2053,7 +2163,7 @@ function transformPureMatch(chain, typeDecls) {
2053
2163
  if (!body)
2054
2164
  return null;
2055
2165
  if (remaining.fields.length > 0)
2056
- body = replaceFieldAccess(body, chain.varName, remaining.fields);
2166
+ body = replaceFieldAccess(body, chain.varName, remaining.fields, remaining.name, tyBaseName(chain.typeName));
2057
2167
  if (isSynthArrayUnion && remaining.fields.length === 1) {
2058
2168
  body = replaceVarInExpr(body, chain.varName, matchBinder(remaining.fields[0].name, chain.varName));
2059
2169
  }
@@ -2066,7 +2176,7 @@ function transformPureMatch(chain, typeDecls) {
2066
2176
  arms.push({ pattern: pWild(), body });
2067
2177
  }
2068
2178
  }
2069
- return { kind: "match", scrutinee: chain.varName, arms };
2179
+ return { kind: "match", scrutinee: varE(chain.varName), arms };
2070
2180
  }
2071
2181
  // ── Generate type declarations ───────────────────────────────
2072
2182
  function transformTypeDecl(d) {
@@ -2203,6 +2313,7 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
2203
2313
  _forofCounters.clear();
2204
2314
  _liftCounter = 0;
2205
2315
  _typeDecls = mod.typeDecls;
2316
+ _neededKindHelpers = new Map();
2206
2317
  _pureDefNames = new Set(mod.functions.filter(f => f.isPure).map(f => f.name));
2207
2318
  const typeDecls = mod.typeDecls.map(transformTypeDecl);
2208
2319
  // Module-level constants
@@ -2272,27 +2383,6 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
2272
2383
  ensures: ext.ensures.map(e => replaceVar(transformExpr(e), "\\result", fnCall)),
2273
2384
  };
2274
2385
  });
2275
- // Types file
2276
- const typesImports = ["LemmaScript"];
2277
- let typesFile = null;
2278
- const pureNamespace = pureDefs.length > 0
2279
- ? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
2280
- : [];
2281
- if (typeDecls.length > 0 || pureDefs.length > 0 || externDecls.length > 0) {
2282
- // Declaration order differs by backend. Dafny allows forward references, so
2283
- // externs go first to be in scope everywhere. Lean requires definition-before-use:
2284
- // an extern's signature may reference a declared type (e.g. `estimateTokens(m: AgentMessage)`),
2285
- // so types must precede externs, which in turn precede the pure mirrors that may call them.
2286
- const decls = _opts.backend === "lean"
2287
- ? [...typeDecls, ...externDecls, ...pureNamespace]
2288
- : [...externDecls, ...typeDecls, ...pureNamespace];
2289
- typesFile = {
2290
- comment: " Generated by lsc — Lean types and pure function mirrors.",
2291
- imports: typesImports,
2292
- options: [],
2293
- decls,
2294
- };
2295
- }
2296
2386
  // Def file: Velvet methods
2297
2387
  // Pure functions get a thin wrapper that calls Pure.fnName
2298
2388
  // def-by-method functions also skip their method wrappers
@@ -2369,6 +2459,106 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
2369
2459
  methods: classMethods,
2370
2460
  };
2371
2461
  });
2462
+ // Types file — assembled after all body transforms, so needs discovered
2463
+ // there (discriminator kind-helpers) are included.
2464
+ const kindHelpers = [..._neededKindHelpers.values()].map(kindHelperDecl);
2465
+ // ── Imported / undeclared user types: opaque by default ─────────────
2466
+ // A module may reference types it imports (ir.ts uses typedir's `Ty`).
2467
+ // Standalone compilation has no declaration for them; synthesize an
2468
+ // opaque type — the value passes through, uninspectable, which is the
2469
+ // only sound use of an undeclared type (same doctrine as _synthOpaque).
2470
+ // Any attempted inspection still fails loudly: an opaque type has no
2471
+ // constructors and no operations. Signature-level coverage (type-decl
2472
+ // fields, params, returns, class fields, externs, consts); a body-level
2473
+ // reference to an undeclared type still errors in the backend.
2474
+ const referenced = new Set();
2475
+ const collectTy = (ty) => {
2476
+ switch (ty.kind) {
2477
+ case "user":
2478
+ referenced.add(tyBaseName(ty.name));
2479
+ return;
2480
+ case "array":
2481
+ case "set":
2482
+ collectTy(ty.elem);
2483
+ return;
2484
+ case "optional":
2485
+ collectTy(ty.inner);
2486
+ return;
2487
+ case "map":
2488
+ collectTy(ty.key);
2489
+ collectTy(ty.value);
2490
+ return;
2491
+ case "tuple":
2492
+ ty.elems.forEach(collectTy);
2493
+ return;
2494
+ case "fn":
2495
+ ty.params.forEach(collectTy);
2496
+ collectTy(ty.result);
2497
+ return;
2498
+ default: return;
2499
+ }
2500
+ };
2501
+ const knownTypeNames = new Set(typeDecls.map(d => d.name));
2502
+ const allTypeParams = new Set();
2503
+ // Exclude type params from the *source* decls — the transformed IR drops
2504
+ // them for aliases (`type Step<S, A> = …`), and a generic alias's params
2505
+ // must not be mistaken for imported types. Params may carry a `//@ type`
2506
+ // decoration ("S(==)"); references collect as the bare name, so strip it.
2507
+ const addTp = (tp) => { allTypeParams.add(tp.replace(/\(.*$/, "").trim()); };
2508
+ for (const d of mod.typeDecls)
2509
+ d.typeParams?.forEach(addTp);
2510
+ for (const d of typeDecls) {
2511
+ if (d.kind === "inductive")
2512
+ d.constructors.forEach(c => c.fields.forEach(f => collectTy(f.type)));
2513
+ else if (d.kind === "structure")
2514
+ d.fields.forEach(f => collectTy(f.type));
2515
+ else if (d.kind === "type-alias")
2516
+ collectTy(d.target);
2517
+ }
2518
+ for (const fn of mod.functions) {
2519
+ fn.typeParams.forEach(addTp);
2520
+ fn.params.forEach(p => collectTy(p.ty));
2521
+ collectTy(fn.returnTy);
2522
+ }
2523
+ for (const cls of mod.classes ?? []) {
2524
+ cls.fields.forEach(f => collectTy(f.ty));
2525
+ for (const m of cls.methods) {
2526
+ m.typeParams.forEach(addTp);
2527
+ m.params.forEach(p => collectTy(p.ty));
2528
+ collectTy(m.returnTy);
2529
+ }
2530
+ }
2531
+ for (const ext of mod.externs ?? []) {
2532
+ ext.typeParams.forEach(addTp);
2533
+ ext.params.forEach(p => collectTy(p.ty));
2534
+ collectTy(ext.returnTy);
2535
+ }
2536
+ for (const c of mod.constants ?? [])
2537
+ collectTy(c.ty);
2538
+ const opaqueImports = [...referenced]
2539
+ .filter(n => !knownTypeNames.has(n) && !allTypeParams.has(n))
2540
+ .map(n => ({ kind: "opaque-type", name: n }));
2541
+ const typesImports = ["LemmaScript"];
2542
+ let typesFile = null;
2543
+ const pureNamespace = pureDefs.length > 0
2544
+ ? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
2545
+ : [];
2546
+ if (typeDecls.length > 0 || pureDefs.length > 0 || externDecls.length > 0 || opaqueImports.length > 0) {
2547
+ // Declaration order differs by backend. Dafny allows forward references, so
2548
+ // externs go first to be in scope everywhere. Lean requires definition-before-use:
2549
+ // an extern's signature may reference a declared type (e.g. `estimateTokens(m: AgentMessage)`),
2550
+ // so types must precede externs, which in turn precede the pure mirrors that may call them.
2551
+ // Kind-helpers sit right after the datatypes they discriminate.
2552
+ const decls = _opts.backend === "lean"
2553
+ ? [...opaqueImports, ...typeDecls, ...kindHelpers, ...externDecls, ...pureNamespace]
2554
+ : [...externDecls, ...opaqueImports, ...typeDecls, ...kindHelpers, ...pureNamespace];
2555
+ typesFile = {
2556
+ comment: " Generated by lsc — Lean types and pure function mirrors.",
2557
+ imports: typesImports,
2558
+ options: [],
2559
+ decls,
2560
+ };
2561
+ }
2372
2562
  const defImport = specImport ?? (typesFile ? `«${moduleBase}.types»` : null);
2373
2563
  const defBaseImports = defImport ? [defImport] : ["LemmaScript"];
2374
2564
  const defFile = {