lemmascript 0.5.1 → 0.5.3

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.
@@ -7,7 +7,22 @@ function tyToLean(ty) {
7
7
  switch (ty.kind) {
8
8
  case "nat": return "Nat";
9
9
  case "int": return "Int";
10
- case "real": return "Float"; // Lean doesn't have exact reals; Float is approximate
10
+ case "real":
11
+ // Real arithmetic isn't supported by the Lean backend yet: ℝ is
12
+ // noncomputable and needs Mathlib's real-number development, so we fail
13
+ // fast here rather than emit Lean that can't compile.
14
+ //
15
+ // Workarounds, in order of preference:
16
+ // 1. If integer division was intended, write `Math.floor(a / b)` — it
17
+ // lowers to flooring integer division on Lean (no real involved).
18
+ // 2. For `bigint` operands, `/` is already integer division — declaring
19
+ // the value `bigint` instead of `number` keeps it off the real path.
20
+ // 3. If the file genuinely needs reals, restrict it to Dafny with a
21
+ // `//@ backend dafny` directive.
22
+ // Full Lean real support is feasible but was set aside: it needs
23
+ // `import Mathlib.Data.Real.Basic`, `noncomputable def`s for real-valued
24
+ // functions, and the Int→ℝ coercion (see the stashed WIP for a sketch).
25
+ throw new Error("real arithmetic is not supported by the Lean backend (needs noncomputable ℝ / Mathlib).");
11
26
  case "bool": return "Bool";
12
27
  case "string": return "String";
13
28
  case "void": return "Unit";
@@ -90,6 +105,16 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
90
105
  return `Array.push ${obj} ${args[0]}`;
91
106
  if (method === "concat")
92
107
  return `Array.push ${obj} ${args[0]}`;
108
+ // arr.slice → Array.extract. No-arg slice is a full copy (Array is a value
109
+ // type in Lean, so the receiver itself); one arg drops the prefix, two args
110
+ // give the half-open range. Matches JS for non-negative bounds (negative
111
+ // indices are unsupported — same caveat as the Dafny backend's direct slice).
112
+ if (method === "slice" && args.length === 0)
113
+ return obj;
114
+ if (method === "slice" && args.length === 1)
115
+ return `${obj}.extract ${args[0]} ${obj}.size`;
116
+ if (method === "slice" && args.length === 2)
117
+ return `${obj}.extract ${args[0]} ${args[1]}`;
93
118
  }
94
119
  // String methods
95
120
  if (tyKind === "string") {
@@ -122,12 +147,17 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
122
147
  throw new Error(`Unsupported Lean method call: .${method}() on ${tyKind}`);
123
148
  }
124
149
  // ── Expression emission ─────────────────────────────────────
125
- // Lean's `∀`/`∃` body extends as far as possible. So `(∃ x, P) <op> Q`
126
- // (or `∃ x, P Q`) would parse with the operator absorbed into the body.
127
- // Wrap a quantifier in parens to terminate its body before the operator.
128
- function wrapQuantifier(sub, parentPrec) {
150
+ // Some Lean term forms extend their body as far as possible: `∀`/`∃` bodies,
151
+ // and `if`/`let` tails. As an operator operand they would swallow the operator
152
+ // `(if c then 1 else 0) + r` written bare parses as `if c then 1 else (0 + r)`.
153
+ // Wrap these forms in parens so the operand is closed before the operator.
154
+ // (`match` self-parenthesizes in `emitExpr`, and other forms close via
155
+ // precedence, so neither needs wrapping here.)
156
+ function wrapOperand(sub, parentPrec) {
129
157
  const inner = emitExpr(sub, parentPrec);
130
- return (sub.kind === "forall" || sub.kind === "exists") ? `(${inner})` : inner;
158
+ return (sub.kind === "forall" || sub.kind === "exists" ||
159
+ sub.kind === "if" || sub.kind === "let")
160
+ ? `(${inner})` : inner;
131
161
  }
132
162
  function emitExpr(e, parentPrec) {
133
163
  switch (e.kind) {
@@ -183,19 +213,30 @@ function emitExpr(e, parentPrec) {
183
213
  return `${wrap ? `(${recv})` : recv}.contains ${emitExpr(e.left)}`;
184
214
  }
185
215
  const op = e.op === "arrayConcat" ? "++" : e.op;
186
- const s = `${wrapQuantifier(e.left, prec(e.op))} ${op} ${emitExpr(e.right, prec(e.op))}`;
216
+ const s = `${wrapOperand(e.left, prec(e.op))} ${op} ${wrapOperand(e.right, prec(e.op))}`;
187
217
  return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
188
218
  }
189
219
  case "implies": {
190
- const parts = [...e.premises.map(p => wrapQuantifier(p)), emitExpr(e.conclusion)];
220
+ const parts = [...e.premises.map(p => wrapOperand(p)), emitExpr(e.conclusion)];
191
221
  const s = parts.join(" → ");
192
222
  return parentPrec !== undefined ? `(${s})` : s;
193
223
  }
194
224
  case "app": {
195
225
  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));
226
+ // Datatype constructor (tagged by transform): Lean needs the qualified name
227
+ // `BaseType.variant`; a bare `variant` is an unknown identifier. (Dafny keeps
228
+ // the bare form, so its output is unaffected.)
229
+ if (e.ctorOf)
230
+ return args.length ? `${e.ctorOf}.${e.fn} ${args.join(" ")}` : `${e.ctorOf}.${e.fn}`;
196
231
  // SetToSeq → .toArray for Lean (HashSet has native toArray)
197
232
  if (e.fn === "SetToSeq" && args.length === 1)
198
233
  return `${args[0]}.toArray`;
234
+ // perm(a, b) → `List.Perm` on the underlying lists. Dafny lowers it to
235
+ // `multiset(a) == multiset(b)`; the Lean image is `a.toList ~ b.toList`,
236
+ // which mathlib's `List.Perm` provides (reflexivity, symmetry,
237
+ // `perm_append_comm`, and `Perm.count_eq` for the count-invariance payoff).
238
+ if (e.fn === "Perm" && args.length === 2)
239
+ return `(${args[0]}.toList).Perm (${args[1]}.toList)`;
199
240
  return `${e.fn} ${args.join(" ")}`;
200
241
  }
201
242
  case "field": {
@@ -210,6 +251,10 @@ function emitExpr(e, parentPrec) {
210
251
  const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
211
252
  return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
212
253
  }
254
+ case "toReal":
255
+ // A real value reached the Lean backend via coercion (e.g. number `/`).
256
+ // Same unsupported-real story as the `real` type case in tyToLean.
257
+ throw new Error("real arithmetic is not supported by the Lean backend (needs noncomputable ℝ / Mathlib).");
213
258
  case "index":
214
259
  return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
215
260
  case "record": {
@@ -393,9 +438,24 @@ function emitDecl(d) {
393
438
  case "type-alias": {
394
439
  return `abbrev ${d.name} := ${tyToLean(d.target)}`;
395
440
  }
441
+ case "opaque-type": {
442
+ // Abstract type — no definition. Never constructed or destructured.
443
+ return `opaque ${d.name} : Type`;
444
+ }
396
445
  case "def": {
397
446
  const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
398
- return `def ${d.name} ${params} : ${tyToLean(d.returnType)} :=\n${emitPureExpr(d.body, 1)}`;
447
+ let out = `def ${d.name} ${params} : ${tyToLean(d.returnType)} :=\n${emitPureExpr(d.body, 1)}`;
448
+ // A `//@ decreases` on a pure function marks it recursive and names its
449
+ // termination measure — emit it as Lean's `termination_by`. This is
450
+ // required when the recursion is on `arr.slice(...)` (→ `Array.extract`,
451
+ // which Lean cannot see as a structural subterm); for a bare-Nat counter
452
+ // Lean could infer structural recursion on its own, but honoring the
453
+ // clause uniformly is simpler and harmless. Lean's default `decreasing_by`
454
+ // discharges the goal in both cases (it knows `Array.size_extract`), so no
455
+ // explicit tactic is needed.
456
+ if (d.decreases)
457
+ out += `\ntermination_by ${emitExpr(d.decreases)}`;
458
+ return out;
399
459
  }
400
460
  case "def-by-method":
401
461
  throw new Error("function by method is not supported for Lean backend");
package/tools/dist/lsc.js CHANGED
@@ -10,6 +10,7 @@ import path from "path";
10
10
  import { extractModule } from "./extract.js";
11
11
  import { resolveModule } from "./resolve.js";
12
12
  import { narrowModule } from "./narrow.js";
13
+ import { autoHavocModule } from "./autohavoc.js";
13
14
  import { transformModuleLean, transformModuleDafny } from "./transform.js";
14
15
  import { peepholeModule } from "./peephole.js";
15
16
  import { emitLeanFile } from "./lean-emit.js";
@@ -95,7 +96,10 @@ function main() {
95
96
  // Resolve: Raw IR → Typed IR
96
97
  const resolved = resolveModule(raw);
97
98
  // Narrow: Typed IR → Typed IR (rewrites optional-narrowing patterns to someMatch)
98
- const typed = narrowModule(resolved);
99
+ // auto-havoc (//@ autohavoc): replace unmodellable expressions with arbitrary
100
+ // values so verification rests only on the declared contracts (a sound
101
+ // over-approximation). No-op unless a function opts in.
102
+ const typed = autoHavocModule(narrowModule(resolved));
99
103
  const dir = path.dirname(absPath);
100
104
  const base = path.basename(filePath, ".ts");
101
105
  // ── Dafny backend ─────────────────────────────────────────
@@ -349,14 +349,18 @@ function ruleConditionalArrayIsArray(e) {
349
349
  if (e.kind !== "conditional")
350
350
  return null;
351
351
  const pos = parseArrayIsArrayCall(e.cond);
352
- const neg = e.cond.kind === "unop" && e.cond.op === "!"
352
+ // `typeof x === "string"` is a positive check like `Array.isArray`, but selects
353
+ // the NonArrayBranch — its then-branch is the matched-variant body.
354
+ const tof = pos ? null : parseTypeofStringCheck(e.cond);
355
+ const neg = !pos && !tof && e.cond.kind === "unop" && e.cond.op === "!"
353
356
  ? parseArrayIsArrayCall(e.cond.expr)
354
357
  : null;
355
- const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
358
+ const matched = pos ?? tof ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
356
359
  if (!matched)
357
360
  return null;
358
- const thenBody = pos ? e.then : e.else;
359
- const elseBody = pos ? e.else : e.then;
361
+ const positive = pos ?? tof;
362
+ const thenBody = positive ? e.then : e.else;
363
+ const elseBody = positive ? e.else : e.then;
360
364
  return {
361
365
  kind: "tagMatch",
362
366
  scrutinee: matched.scrutinee,
@@ -602,6 +606,30 @@ function parseArrayIsArrayCall(call) {
602
606
  return null;
603
607
  return { scrutinee: arg, typeName: arg.ty.name, variant: "ArrayBranch" };
604
608
  }
609
+ /** Detect `typeof <path> === "string"` where `<path>`'s type is a synth array-
610
+ * union (`U | T[]`) AND its `NonArrayBranch` payload `U` is itself `string`.
611
+ * The runtime `=== "string"` test matches that branch only when `U` is string —
612
+ * for any other non-array payload (`number | T[]`, …) it never holds, so we must
613
+ * NOT narrow. Returns the `NonArrayBranch` variant; the dual of `Array.isArray`. */
614
+ function parseTypeofStringCheck(e) {
615
+ if (e.kind !== "binop" || e.op !== "===")
616
+ return null;
617
+ const tof = e.left.kind === "unop" && e.left.op === "typeof" ? e.left.expr
618
+ : e.right.kind === "unop" && e.right.op === "typeof" ? e.right.expr : null;
619
+ const lit = e.left.kind === "str" ? e.left.value : e.right.kind === "str" ? e.right.value : null;
620
+ if (!tof || lit !== "string")
621
+ return null;
622
+ if (!isNarrowablePath(tof) || tof.ty.kind !== "user")
623
+ return null;
624
+ const baseTyName = tof.ty.name.includes("<") ? tof.ty.name.slice(0, tof.ty.name.indexOf("<")) : tof.ty.name;
625
+ const decl = _typeDecls.find(d => d.name === baseTyName);
626
+ if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
627
+ return null;
628
+ const valTy = decl.variants?.find(v => v.name === "NonArrayBranch")?.fields.find(f => f.name === "val")?.type;
629
+ if (valTy?.kind !== "string")
630
+ return null; // guard: the non-array branch must actually be `string`
631
+ return { scrutinee: tof, typeName: tof.ty.name, variant: "NonArrayBranch" };
632
+ }
605
633
  /** A "narrowable path" is a var or a chain of field accesses rooted at a var
606
634
  * — i.e., pure and structurally addressable, so transforms can substitute
607
635
  * occurrences inside a matched arm without worrying about re-evaluation. */
@@ -20,6 +20,7 @@ function mapExpr(e, f) {
20
20
  case "app": return { ...e, args: e.args.map(r) };
21
21
  case "field": return { ...e, obj: r(e.obj) };
22
22
  case "toNat": return { ...e, expr: r(e.expr) };
23
+ case "toReal": return { ...e, expr: r(e.expr) };
23
24
  case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
24
25
  case "record": return { ...e, spread: e.spread ? r(e.spread) : null,
25
26
  fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
@@ -364,6 +365,7 @@ function rewriteChildrenExpr(e) {
364
365
  case "app": return { ...e, args: e.args.map(r) };
365
366
  case "field": return { ...e, obj: r(e.obj) };
366
367
  case "toNat": return { ...e, expr: r(e.expr) };
368
+ case "toReal": return { ...e, expr: r(e.expr) };
367
369
  case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
368
370
  case "record": return { ...e, spread: e.spread ? r(e.spread) : null,
369
371
  fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
@@ -445,6 +447,7 @@ function peepholeDecl(d) {
445
447
  case "inductive":
446
448
  case "structure":
447
449
  case "type-alias":
450
+ case "opaque-type":
448
451
  case "extern":
449
452
  return d;
450
453
  }
@@ -4,6 +4,7 @@
4
4
  * Uses linked environments (Scheme-style) for lexical scoping.
5
5
  * No mutation — each let extends the chain, lookup walks it.
6
6
  */
7
+ import { isBigInt } from "./typedir.js";
7
8
  import { parseTsType } from "./types.js";
8
9
  import { parseExpr } from "./specparser.js";
9
10
  function lookup(env, name) {
@@ -447,6 +448,12 @@ function tyToTsStr(ty) {
447
448
  return "number";
448
449
  if (ty.kind === "bool")
449
450
  return "boolean";
451
+ // Optional element (e.g. a `.filter` over a `T | undefined`-typed map result):
452
+ // type the callback param so its `x !== undefined` check narrows correctly.
453
+ if (ty.kind === "optional") {
454
+ const inner = tyToTsStr(ty.inner);
455
+ return inner ? `${inner} | undefined` : undefined;
456
+ }
450
457
  return undefined;
451
458
  }
452
459
  function inferLambdaParamTypes(fn, rawArgs, ctx) {
@@ -486,6 +493,21 @@ function inferLambdaParamTypes(fn, rawArgs, ctx) {
486
493
  }
487
494
  return rawArgs;
488
495
  }
496
+ /** A defined-check filter predicate: `(x) => x !== undefined` (expression or
497
+ * single-return body). Detected on the raw IR before narrowing rewrites it. */
498
+ function isDefinedCheckRawLambda(raw) {
499
+ if (raw.kind !== "lambda" || raw.params.length !== 1)
500
+ return false;
501
+ const p = raw.params[0].name;
502
+ const body = Array.isArray(raw.body)
503
+ ? (raw.body.length === 1 && raw.body[0].kind === "return" ? raw.body[0].value : null)
504
+ : raw.body;
505
+ if (!body || body.kind !== "binop" || body.op !== "!==")
506
+ return false;
507
+ const isParam = (x) => x.kind === "var" && x.name === p;
508
+ const isUndef = (x) => x.kind === "var" && x.name === "undefined";
509
+ return (isParam(body.left) && isUndef(body.right)) || (isParam(body.right) && isUndef(body.left));
510
+ }
489
511
  /** Coerce call arguments: string literals → user types, non-optional → Some, pad missing optional args. */
490
512
  function coerceCallArgs(args, fn, ctx) {
491
513
  if (fn.kind !== "var" || !ctx.fnParams.has(fn.name))
@@ -517,6 +539,16 @@ function inferMethodReturnTy(fn, args, ctx) {
517
539
  if (fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray") {
518
540
  return { kind: "bool" };
519
541
  }
542
+ // Math.* numeric builtins: abs/min/max preserve the operand's numeric type
543
+ // (real if any operand is real); floor/ceil/round/trunc return an integer.
544
+ if (fn.obj.kind === "var" && fn.obj.name === "Math") {
545
+ if (fn.field === "abs" && args.length === 1)
546
+ return args[0].ty;
547
+ if ((fn.field === "min" || fn.field === "max") && args.length >= 1)
548
+ return args.some(a => a.ty.kind === "real") ? { kind: "real" } : args[0].ty;
549
+ if (["floor", "ceil", "round", "trunc"].includes(fn.field))
550
+ return { kind: "int" };
551
+ }
520
552
  const objTy = fn.obj.ty;
521
553
  if (objTy.kind === "map") {
522
554
  if (fn.field === "get")
@@ -558,8 +590,11 @@ function inferMethodReturnTy(fn, args, ctx) {
558
590
  if (fn.field === "join" && objTy.elem.kind === "string")
559
591
  return { kind: "string" };
560
592
  if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
561
- const retTy = args[0].body.length > 0 && args[0].body[0].kind === "return"
562
- ? args[0].body[0].value.ty : { kind: "unknown" };
593
+ const lam = args[0];
594
+ // Prefer the lambda's declared return type (handles multi-statement bodies
595
+ // where body[0] is an `if`, not a `return`); fall back to the body's return.
596
+ const retTy = lam.ty.kind === "fn" ? lam.ty.result
597
+ : lam.body.length > 0 && lam.body[0].kind === "return" ? lam.body[0].value.ty : { kind: "unknown" };
563
598
  return { kind: "array", elem: retTy };
564
599
  }
565
600
  }
@@ -613,6 +648,8 @@ function resolveExpr(e, ctx) {
613
648
  case "num":
614
649
  if (!Number.isInteger(e.value))
615
650
  return { kind: "num", value: e.value, ty: { kind: "real" } };
651
+ if (e.big)
652
+ return { kind: "num", value: e.value, ty: { kind: "int", big: true } };
616
653
  return { kind: "num", value: e.value, ty: e.value >= 0 ? { kind: "nat" } : { kind: "int" } };
617
654
  case "str":
618
655
  return { kind: "str", value: e.value, ty: { kind: "string" } };
@@ -658,16 +695,38 @@ function resolveExpr(e, ctx) {
658
695
  }
659
696
  else if (e.op === "||")
660
697
  ty = right.ty;
661
- else if (["+", "-", "*", "/", "%"].includes(e.op)) {
698
+ else if (e.op === "/") {
699
+ // `number / number` is real (floating-point) division: 3 / 2 === 1.5,
700
+ // never 1 — an integer quotient requires an explicit Math.floor (which
701
+ // lowers to JSFloorDiv). But `bigint / bigint` is genuinely integer
702
+ // division in JS (3n / 2n === 1n), so keep it integer.
703
+ ty = (isBigInt(left.ty) || isBigInt(right.ty)) ? { kind: "int", big: true } : { kind: "real" };
704
+ }
705
+ else if (["+", "-", "*", "%"].includes(e.op)) {
662
706
  ty = (left.ty.kind === "real" || right.ty.kind === "real") ? { kind: "real" } : left.ty;
663
707
  }
664
708
  return { kind: "binop", op: e.op, left, right, ty };
665
709
  }
666
710
  case "unop": {
667
711
  const expr = resolveExpr(e.expr, ctx);
668
- return { kind: "unop", op: e.op, expr, ty: e.op === "!" ? { kind: "bool" } : expr.ty };
712
+ const ty = e.op === "!" ? { kind: "bool" } : e.op === "typeof" ? { kind: "string" } : expr.ty;
713
+ return { kind: "unop", op: e.op, expr, ty };
669
714
  }
670
715
  case "call": {
716
+ // perm(a, b): spec-only permutation predicate — true iff `a` and `b` are
717
+ // reorderings of each other (equal as multisets). Lowers to the `Perm`
718
+ // preamble (Dafny `multiset(a) == multiset(b)`; Lean `a.toList ~ b.toList`).
719
+ // It has no runtime counterpart, so it is rejected outside `//@` specs.
720
+ if (e.fn.kind === "var" && e.fn.name === "perm" && e.args.length === 2) {
721
+ if (!ctx.inSpec)
722
+ throw new Error("perm(a, b) may only be used in //@ specifications");
723
+ const a = resolveExpr(e.args[0], ctx);
724
+ const b = resolveExpr(e.args[1], ctx);
725
+ if (a.ty.kind !== "array" || b.ty.kind !== "array")
726
+ throw new Error(`perm(a, b) requires two array arguments (got ${a.ty.kind} and ${b.ty.kind})`);
727
+ const fn = { kind: "var", name: "Perm", ty: { kind: "unknown" } };
728
+ return { kind: "call", fn, args: [a, b], ty: { kind: "bool" }, callKind: "pure" };
729
+ }
671
730
  // Extern dispatch: `NS.method(args)` where NS.method is declared via
672
731
  // `//@ extern`. Rewrite into a flat-name call (`NS_method(args)`) so the
673
732
  // rest of the pipeline sees an ordinary pure function. The extern's
@@ -711,6 +770,16 @@ function resolveExpr(e, ctx) {
711
770
  if (ty.kind === "unknown" && fn.kind === "var" && ctx.fnReturns.has(fn.name)) {
712
771
  ty = ctx.fnReturns.get(fn.name);
713
772
  }
773
+ // filterMap: `seqOfOption.filter(x => x !== undefined)` (a defined-check,
774
+ // typically with an `x is T` type guard) drops the Nones AND unwraps to
775
+ // seq<T>. Rewrite to a synthetic `filterSome` call lowered to the proven
776
+ // SeqFilterSome preamble (a plain `Map(.value, Filter(.Some?))` wouldn't
777
+ // verify — `.value` is partial).
778
+ if (e.fn.kind === "field" && e.fn.field === "filter" && e.args.length === 1
779
+ && isDefinedCheckRawLambda(e.args[0])
780
+ && fn.kind === "field" && fn.obj.ty.kind === "array" && fn.obj.ty.elem.kind === "optional") {
781
+ return { kind: "call", fn: { ...fn, field: "filterSome" }, args: [], ty: { kind: "array", elem: fn.obj.ty.elem.inner }, callKind: "method" };
782
+ }
714
783
  return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
715
784
  }
716
785
  case "index": {
@@ -881,7 +950,13 @@ function resolveExpr(e, ctx) {
881
950
  return { kind: "exists", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
882
951
  }
883
952
  case "arrayLiteral": {
884
- const elems = e.elems.map(el => resolveExpr(el, ctx));
953
+ // Thread the expected element type into each element, so a record/union
954
+ // literal in an array resolves to its named datatype rather than an
955
+ // anonymous tuple (mirrors return-position and call-argument records, which
956
+ // get their type via ctx.returnTy). Only narrow when the context type is an
957
+ // array; otherwise leave ctx untouched.
958
+ const elemCtx = ctx.returnTy.kind === "array" ? { ...ctx, returnTy: ctx.returnTy.elem } : ctx;
959
+ const elems = e.elems.map(el => resolveExpr(el, elemCtx));
885
960
  const elemTy = elems.length > 0 ? elems[0].ty : { kind: "unknown" };
886
961
  return { kind: "arrayLiteral", elems, ty: { kind: "array", elem: elemTy } };
887
962
  }
@@ -895,12 +970,21 @@ function resolveExpr(e, ctx) {
895
970
  let lambdaEnv = ctx.env;
896
971
  for (const p of params)
897
972
  lambdaEnv = extend(lambdaEnv, p.name, p.ty);
898
- const lambdaCtx = { ...withEnv(ctx, lambdaEnv), inLambda: true };
973
+ // Set returnTy to the lambda's own return annotation (not the enclosing
974
+ // function's), so return-position record literals in the body resolve to
975
+ // their named type rather than an anonymous tuple.
976
+ const lambdaReturnTy = e.returnTsType ? parseTsType(e.returnTsType) : { kind: "unknown" };
977
+ const lambdaCtx = { ...withEnv(ctx, lambdaEnv), inLambda: true, returnTy: lambdaReturnTy };
899
978
  // Body: expression (wrap in return stmt) or statement block
900
979
  const body = Array.isArray(e.body)
901
980
  ? resolveBlock(e.body, lambdaCtx)
902
981
  : [{ kind: "return", value: resolveExpr(e.body, lambdaCtx) }];
903
- return { kind: "lambda", params, body, ty: { kind: "unknown" } };
982
+ // Carry the lambda's type as a fn type when its return is known, so chained
983
+ // array methods (`.map(...).filter(...)`) can infer downstream element types.
984
+ const lamTy = e.returnTsType
985
+ ? { kind: "fn", params: params.map(p => p.ty), result: lambdaReturnTy }
986
+ : { kind: "unknown" };
987
+ return { kind: "lambda", params, body, ty: lamTy };
904
988
  }
905
989
  case "conditional": {
906
990
  const cond = resolveExpr(e.cond, ctx);
@@ -1028,9 +1112,11 @@ function resolveStmt(s, ctx) {
1028
1112
  // to its underlying type, so array methods / index-assignment on the
1029
1113
  // local dispatch correctly (params get the same treatment, see makeParams).
1030
1114
  const declTy = expandAlias(resolveTsType(s.tsType, ctx.overrides, s.name), ctx.typeDecls);
1031
- // Propagate declared type as returnTy so nested record expressions
1032
- // resolve union variants correctly (e.g., EffectState → mode: EffectMode → { kind: 'Idle' })
1033
- const initCtx = declTy.kind === "user" ? { ...ctx, returnTy: declTy } : ctx;
1115
+ // Propagate declared type as returnTy so nested record expressions resolve
1116
+ // union variants correctly (e.g., EffectState → mode: EffectMode → { kind:
1117
+ // 'Idle' }). Arrays too, so `const xs: Foo[] = [{...}]` threads the element
1118
+ // type into the array literal (see the arrayLiteral case).
1119
+ const initCtx = (declTy.kind === "user" || declTy.kind === "array") ? { ...ctx, returnTy: declTy } : ctx;
1034
1120
  const init = coerceStr(resolveExpr(s.init, initCtx), declTy);
1035
1121
  let ty;
1036
1122
  if (isUnmodeledTy(declTy, ctx.typeDecls) && !isUnmodeledTy(init.ty, ctx.typeDecls)) {
@@ -1042,6 +1128,12 @@ function resolveStmt(s, ctx) {
1042
1128
  ? { kind: "optional", inner: init.ty }
1043
1129
  : init.ty;
1044
1130
  }
1131
+ else if ((declTy.kind === "int" || declTy.kind === "nat") && init.ty.kind === "real" && !ctx.overrides.has(s.name)) {
1132
+ // TS infers `number` (→ int/nat) for an expression LS computes as `real`
1133
+ // (e.g. `a / b`, now real division). `number` can't tell them apart, so
1134
+ // trust the real-valued initializer — unless the user pinned the type.
1135
+ ty = init.ty;
1136
+ }
1045
1137
  else {
1046
1138
  // Map indexing: TS says T, but access can fail → use Optional<T> from init
1047
1139
  ty = (declTy.kind !== "optional" && init.ty.kind === "optional") ? init.ty : declTy;
@@ -1381,6 +1473,7 @@ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), fnReturns
1381
1473
  decreases: fn.decreases ? resolveSpec(fn.decreases, requiresCtx) : null,
1382
1474
  isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
1383
1475
  forcePure: fn.pure,
1476
+ autohavoc: fn.autohavoc,
1384
1477
  body: resolveBlock(fn.body, bodyCtx),
1385
1478
  };
1386
1479
  }
@@ -32,6 +32,7 @@ function mapExpr(e, f) {
32
32
  case "app": return { ...e, args: e.args.map(r) };
33
33
  case "field": return { ...e, obj: r(e.obj) };
34
34
  case "toNat": return { ...e, expr: r(e.expr) };
35
+ case "toReal": return { ...e, expr: r(e.expr) };
35
36
  case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
36
37
  case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
37
38
  case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
@@ -154,6 +155,7 @@ function buildMatchPattern(variantName, fields, scopePrefix) {
154
155
  }
155
156
  const _forofCounters = new Map();
156
157
  function isNat(ty) { return ty.kind === "nat"; }
158
+ function isIntegral(ty) { return ty.kind === "int" || ty.kind === "nat"; }
157
159
  function isArray(ty) { return ty.kind === "array"; }
158
160
  function isUser(ty) { return ty.kind === "user"; }
159
161
  /** Check if transformed lambda body contains monadic binds. */
@@ -186,6 +188,8 @@ const OP_MAP = {
186
188
  const BOOL_OP_MAP = {
187
189
  ...OP_MAP, "===": "==", "!==": "!=",
188
190
  };
191
+ /** Arithmetic + comparison ops eligible for int→real operand coercion. */
192
+ const NUMERIC_OPS = new Set(["+", "-", "*", "/", "===", "!==", ">=", "<=", ">", "<"]);
189
193
  function transformExpr(e) { return lowerExpr(e, null); }
190
194
  /** Reduce an if/let/return-shaped statement body to a single expression, for
191
195
  * expression-only lambda bodies. Returns null for shapes that can't be a pure
@@ -219,6 +223,19 @@ function flattenLambdaBody(stmts) {
219
223
  const elseExpr = flattenLambdaBody(first.else.length > 0 ? [...first.else, ...rest] : rest);
220
224
  return thenExpr === null || elseExpr === null ? null : { kind: "if", cond: first.cond, then: thenExpr, else: elseExpr };
221
225
  }
226
+ // A `switch` lowered to a match-statement: reduce each arm's body to an
227
+ // expression (an arm that doesn't return falls through into `rest`), giving a
228
+ // match-expression — same reduction the `if` case does, one level wider.
229
+ if (first.kind === "match") {
230
+ const arms = [];
231
+ for (const arm of first.arms) {
232
+ const armExpr = flattenLambdaBody([...arm.body, ...rest]);
233
+ if (armExpr === null)
234
+ return null;
235
+ arms.push({ pattern: arm.pattern, body: armExpr });
236
+ }
237
+ return { kind: "match", scrutinee: first.scrutinee, arms };
238
+ }
222
239
  return null;
223
240
  }
224
241
  /**
@@ -419,6 +436,20 @@ function lowerExpr(e, binds) {
419
436
  right: { kind: "app", fn: "NatToString", args: [lowerExpr(e.right, binds)] } };
420
437
  }
421
438
  }
439
+ // Numeric int→real coercion. After resolve, `/` is always real, and any
440
+ // arithmetic/comparison mixing real and integral operands is real-valued.
441
+ // Lift each integral operand to `real` so the backend sees homogeneous
442
+ // real operations (Dafny `as real`, Lean Int→Float).
443
+ if (NUMERIC_OPS.has(e.op)) {
444
+ const realCtx = e.ty.kind === "real" || e.left.ty.kind === "real" || e.right.ty.kind === "real";
445
+ if (realCtx) {
446
+ const lift = (operand) => {
447
+ const lowered = lowerExpr(operand, binds);
448
+ return isIntegral(operand.ty) ? { kind: "toReal", expr: lowered } : lowered;
449
+ };
450
+ return { kind: "binop", op: OP_MAP[e.op] ?? e.op, left: lift(e.left), right: lift(e.right) };
451
+ }
452
+ }
422
453
  return {
423
454
  kind: "binop",
424
455
  op: OP_MAP[e.op] ?? e.op,
@@ -494,13 +525,23 @@ function lowerExpr(e, binds) {
494
525
  return { kind: "app", fn: "CeilReal", args: [lowerExpr(arg, binds)] };
495
526
  return lowerExpr(arg, binds);
496
527
  }
497
- // Math.floor(x): FloorReal on real args, JSFloorDiv for int division, identity on int
528
+ // Math.floor(x):
529
+ // - a / b on integral operands → integer floor division, kept in
530
+ // integer arithmetic (JSFloorDiv on Dafny; native Int/Nat `/` floors
531
+ // on Lean). Checked first: after resolve, `a / b` is typed `real`, so
532
+ // the real branch below would otherwise drag it into real arithmetic.
533
+ // - real arg → FloorReal (Dafny's .Floor)
534
+ // - int arg → identity
498
535
  if (e.fn.kind === "field" && e.fn.field === "floor" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
499
536
  const arg = e.args[0];
537
+ if (arg.kind === "binop" && arg.op === "/" && isIntegral(arg.left.ty) && isIntegral(arg.right.ty)) {
538
+ const l = lowerExpr(arg.left, binds), r = lowerExpr(arg.right, binds);
539
+ return _opts.backend === "dafny"
540
+ ? { kind: "app", fn: "JSFloorDiv", args: [l, r] }
541
+ : { kind: "binop", op: "/", left: l, right: r };
542
+ }
500
543
  if (arg.ty.kind === "real")
501
544
  return { kind: "app", fn: "FloorReal", args: [lowerExpr(arg, binds)] };
502
- if (_opts.backend === "dafny" && arg.kind === "binop" && arg.op === "/")
503
- return { kind: "app", fn: "JSFloorDiv", args: [lowerExpr(arg.left, binds), lowerExpr(arg.right, binds)] };
504
545
  return lowerExpr(arg, binds);
505
546
  }
506
547
  // Method call: receiver.method(args) → methodCall node
@@ -564,12 +605,17 @@ function lowerExpr(e, binds) {
564
605
  if (nonDiscFields.length === 0) {
565
606
  return { kind: "constructor", name: variantName, type: tyName };
566
607
  }
567
- // Constructor with args: match variant field order
608
+ // Constructor with args: match variant field order. Emit a bare `app`
609
+ // (Dafny renders `variantName(args)`, a valid unqualified constructor
610
+ // call — unchanged output) tagged with `ctorOf` so the Lean emitter,
611
+ // which CANNOT take a bare constructor name, qualifies it as
612
+ // `BaseType.variantName args`. Use the BASE type name (no generic args):
613
+ // `Result.true_` is valid in Lean; `Result<Model,Err>.true_` is not.
568
614
  const args = variant.fields.map(vf => {
569
615
  const ef = nonDiscFields.find(f => f.name === vf.name);
570
616
  return ef ? lowerExpr(ef.value, binds) : { kind: "var", name: "None" };
571
617
  });
572
- return { kind: "app", fn: variantName, args };
618
+ return { kind: "app", fn: variantName, args, ctorOf: baseName };
573
619
  }
574
620
  }
575
621
  }
@@ -642,7 +688,10 @@ function lowerExpr(e, binds) {
642
688
  return { kind: "app", fn: "SetLiteral", args: e.elems.map(el => lowerExpr(el, binds)) };
643
689
  return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
644
690
  case "lambda": {
645
- const body = transformStmts(e.body, []);
691
+ // Pass the module typeDecls (not []), so type lookups inside the lambda
692
+ // body — e.g. a `switch`'s variant fields — resolve. A bare `[]` left a
693
+ // discriminated-union switch in a lambda with binderless patterns.
694
+ const body = transformStmts(e.body, _typeDecls);
646
695
  // Flatten an if/let/return-shaped multi-statement body into a single
647
696
  // `return <expr>` so both backends' single-return-lambda fast path emits
648
697
  // it (Dafny lambdas are expression-only; Lean prefers the expression form
@@ -1321,14 +1370,37 @@ function remainingVariant(typeName, cases, typeDecls) {
1321
1370
  return null;
1322
1371
  return remaining[0];
1323
1372
  }
1373
+ /** `switch(obj.field)` is stripped at extraction to scrutinee `obj` + discriminant
1374
+ * `field`, assuming `obj` is a discriminated union with `field` as its
1375
+ * discriminant. When that's NOT so — e.g. `obj` is a plain record with an
1376
+ * enum-typed `field` — the switch is really on the enum VALUE. This returns the
1377
+ * enum scrutinee `obj.field` (+ the field's enum type) to match directly; null
1378
+ * for a genuine discriminant switch or `switch(localVar)`, which callers handle
1379
+ * their usual way. Shared by emitSwitchStmt and transformPureSwitch. */
1380
+ function enumFieldSwitch(s, typeDecls) {
1381
+ if (!s.discriminant)
1382
+ return null;
1383
+ const objBase = s.expr.ty.kind === "user"
1384
+ ? (s.expr.ty.name.includes("<") ? s.expr.ty.name.slice(0, s.expr.ty.name.indexOf("<")) : s.expr.ty.name)
1385
+ : undefined;
1386
+ const objDecl = objBase ? typeDecls.find(d => d.name === objBase) : undefined;
1387
+ if (objDecl?.kind === "discriminated-union" && objDecl.discriminant === s.discriminant)
1388
+ return null;
1389
+ const fieldTy = objDecl?.kind === "record" ? objDecl.fields?.find(f => f.name === s.discriminant)?.type : undefined;
1390
+ return {
1391
+ scrutinee: { kind: "field", obj: transformExpr(s.expr), field: s.discriminant },
1392
+ enumTyName: fieldTy?.kind === "user" ? fieldTy.name : undefined,
1393
+ };
1394
+ }
1324
1395
  function emitSwitchStmt(s, typeDecls) {
1325
- const varName = s.expr.kind === "var" ? s.expr.name : "?";
1326
- const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
1327
1396
  const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
1328
- const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
1397
+ const ef = enumFieldSwitch(s, typeDecls);
1398
+ const arms = ef
1399
+ ? buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformStmts(body, typeDecls))
1400
+ : 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));
1329
1401
  if (s.defaultBody.length > 0)
1330
1402
  arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
1331
- return { kind: "match", scrutinee: varName, arms };
1403
+ return { kind: "match", scrutinee: ef ? ef.scrutinee : (s.expr.kind === "var" ? s.expr.name : "?"), arms };
1332
1404
  }
1333
1405
  /** Replace obj.field → replacement var in typed IR.
1334
1406
  * Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
@@ -1472,6 +1544,20 @@ function transformPureBody(stmts, typeDecls) {
1472
1544
  return null;
1473
1545
  }
1474
1546
  function transformPureSwitch(s, typeDecls) {
1547
+ const ef = enumFieldSwitch(s, typeDecls);
1548
+ if (ef) {
1549
+ const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
1550
+ const arms = buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformPureBody(body, typeDecls));
1551
+ if (!arms)
1552
+ return null;
1553
+ if (s.defaultBody.length > 0) {
1554
+ const body = transformPureBody(s.defaultBody, typeDecls);
1555
+ if (!body)
1556
+ return null;
1557
+ arms.push({ pattern: "_", body });
1558
+ }
1559
+ return { kind: "match", scrutinee: ef.scrutinee, arms };
1560
+ }
1475
1561
  const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
1476
1562
  if (!typeDecls.find(d => d.name === typeName))
1477
1563
  return null;
@@ -1570,6 +1656,9 @@ function transformTypeDecl(d) {
1570
1656
  target: d.aliasOfTy,
1571
1657
  };
1572
1658
  }
1659
+ else if (d.kind === "opaque") {
1660
+ return { kind: "opaque-type", name: d.name };
1661
+ }
1573
1662
  else {
1574
1663
  return {
1575
1664
  kind: "structure", name: d.name,