lemmascript 0.5.5 → 0.5.7

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.
@@ -158,6 +158,24 @@ function isNat(ty) { return ty.kind === "nat"; }
158
158
  function isIntegral(ty) { return ty.kind === "int" || ty.kind === "nat"; }
159
159
  function isArray(ty) { return ty.kind === "array"; }
160
160
  function isUser(ty) { return ty.kind === "user"; }
161
+ /** Truthiness test for a *lowered* value of source type `ty`, used by `||`
162
+ * falsiness lowering. Mirrors narrow.ts's `canBeFalsy`: only int/nat/string/bool
163
+ * values can be falsy in JS (`0`, `""`, `false`); every other value (array, user
164
+ * type, …) is always truthy. Returns null for the always-truthy types so callers
165
+ * can unwrap directly instead of emitting a redundant guard. */
166
+ function valueTruthyCond(value, ty) {
167
+ switch (ty.kind) {
168
+ case "int":
169
+ case "nat":
170
+ return { kind: "binop", op: "≠", left: value, right: { kind: "num", value: 0 } };
171
+ case "string":
172
+ return { kind: "binop", op: ">", left: { kind: "field", obj: value, field: "length" }, right: { kind: "num", value: 0 } };
173
+ case "bool":
174
+ return value;
175
+ default:
176
+ return null;
177
+ }
178
+ }
161
179
  /** Check if transformed lambda body contains monadic binds. */
162
180
  function isMonadicBody(stmts) {
163
181
  for (const s of stmts) {
@@ -182,7 +200,7 @@ function isMonadicBody(stmts) {
182
200
  const OP_MAP = {
183
201
  "===": "=", "!==": "≠", ">=": "≥", "<=": "≤", ">": ">", "<": "<",
184
202
  "&&": "∧", "||": "∨", "+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
185
- "==": "=", "!=": "≠",
203
+ "==": "=", "!=": "≠", "<==>": "↔",
186
204
  };
187
205
  /** Bool-valued operators (for code-level conditions needing Decidable). */
188
206
  const BOOL_OP_MAP = {
@@ -275,6 +293,55 @@ function wrapOptionalBranch(expr, raw) {
275
293
  return expr; // already Option<T>, don't double-wrap
276
294
  return { kind: "constructor", name: "some", type: "Option", args: [expr] };
277
295
  }
296
+ /** Lean needs `let mut` for any local that is later reassigned. A const local
297
+ * whose collection field is mutated (`b.items.push(v)` → `b := b.(items := …)`)
298
+ * becomes an assign in the lowered body, so scan for assign targets and flip
299
+ * matching lets to mutable. Harmless on Dafny (method locals are `var`); and an
300
+ * assigned let already forces a method, so purity is unaffected. */
301
+ function promoteAssignedLets(stmts) {
302
+ const assigned = new Set();
303
+ const collect = (ss) => {
304
+ for (const s of ss) {
305
+ if (s.kind === "assign")
306
+ assigned.add(s.target);
307
+ else if (s.kind === "if") {
308
+ collect(s.then);
309
+ collect(s.else);
310
+ }
311
+ else if (s.kind === "while" || s.kind === "forin")
312
+ collect(s.body);
313
+ else if (s.kind === "match")
314
+ s.arms.forEach(a => collect(a.body));
315
+ }
316
+ };
317
+ collect(stmts);
318
+ if (assigned.size === 0)
319
+ return stmts;
320
+ const fix = (ss) => ss.map(s => {
321
+ const s2 = s.kind === "let" && !s.mutable && assigned.has(s.name) ? { ...s, mutable: true } : s;
322
+ if (s2.kind === "if")
323
+ return { ...s2, then: fix(s2.then), else: fix(s2.else) };
324
+ if (s2.kind === "while" || s2.kind === "forin")
325
+ return { ...s2, body: fix(s2.body) };
326
+ if (s2.kind === "match")
327
+ return { ...s2, arms: s2.arms.map(a => ({ ...a, body: fix(a.body) })) };
328
+ return s2;
329
+ });
330
+ return fix(stmts);
331
+ }
332
+ /** Build a nested record-update assigning `newVal` to a field-path receiver
333
+ * rooted at a var: `b.a.items` → `b := b.(a := b.a.(items := newVal))`.
334
+ * Returns null if the path isn't a chain of field accesses ending at a var. */
335
+ function buildNestedFieldUpdate(recv, newVal) {
336
+ if (recv.kind !== "field")
337
+ return null;
338
+ const upd = { kind: "record", spread: lowerExpr(recv.obj, null), fields: [{ name: recv.field, value: newVal }] };
339
+ if (recv.obj.kind === "var")
340
+ return { root: recv.obj.name, value: upd };
341
+ if (recv.obj.kind === "field")
342
+ return buildNestedFieldUpdate(recv.obj, upd);
343
+ return null;
344
+ }
278
345
  function lowerExpr(e, binds) {
279
346
  // Monadic lifting: extract embedded method calls to let-binds.
280
347
  // `callKind: "method"` means a global var-fn call (classifyCall returns
@@ -301,13 +368,17 @@ function lowerExpr(e, binds) {
301
368
  // String truthiness: !str → str == ""
302
369
  if (e.op === "!" && e.expr.ty.kind === "string")
303
370
  return { kind: "binop", op: "=", left: lowerExpr(e.expr, binds), right: { kind: "str", value: "" } };
304
- // Optional truthiness: !opt → opt is None
371
+ // Optional truthiness: !opt → None negates to `true`. The Some branch is
372
+ // `!(value truthy)`: always-truthy inners (array/user) give a plain `false`,
373
+ // while falsy-capable inners re-test the wrapped value (`!Some(0)` is `true`).
374
+ // Mirrors the `||` falsiness rule.
305
375
  if (e.op === "!" && e.expr.ty.kind === "optional") {
306
376
  const bound = matchBinder("value");
377
+ const truthy = valueTruthyCond({ kind: "var", name: bound }, e.expr.ty.inner);
307
378
  return {
308
379
  kind: "match", scrutinee: lowerExpr(e.expr, binds),
309
380
  arms: [
310
- { pattern: `.some ${bound}`, body: { kind: "bool", value: false } },
381
+ { pattern: `.some ${bound}`, body: truthy ? { kind: "unop", op: "¬", expr: truthy } : { kind: "bool", value: false } },
311
382
  { pattern: ".none", body: { kind: "bool", value: true } },
312
383
  ],
313
384
  };
@@ -383,37 +454,76 @@ function lowerExpr(e, binds) {
383
454
  ],
384
455
  };
385
456
  }
386
- // || undefined on optional → identity (no-op: x || undefined = x)
457
+ // || undefined on optional → identity (no-op: x || undefined = x) when the
458
+ // inner type is always truthy. When it can be falsy, JS still drops the
459
+ // wrapped value: `Some(0) || undefined === undefined`, so the Some arm
460
+ // re-tests and falls back to None.
387
461
  if (e.op === "||" && e.left.ty.kind === "optional" &&
388
462
  e.right.kind === "var" && e.right.name === "undefined") {
389
- return lowerExpr(e.left, binds);
463
+ const optExpr = lowerExpr(e.left, binds);
464
+ const bound = matchBinder("value");
465
+ const truthy = valueTruthyCond({ kind: "var", name: bound }, e.left.ty.inner);
466
+ if (!truthy)
467
+ return optExpr;
468
+ return {
469
+ kind: "match", scrutinee: optExpr,
470
+ arms: [
471
+ { pattern: `.some ${bound}`, body: {
472
+ kind: "if", cond: truthy,
473
+ then: { kind: "app", fn: "Some", args: [{ kind: "var", name: bound }] },
474
+ else: { kind: "var", name: "undefined" }
475
+ } },
476
+ { pattern: ".none", body: { kind: "var", name: "undefined" } },
477
+ ],
478
+ };
390
479
  }
391
- // || on optional → match Some/None with default
480
+ // || on optional → match Some/None with default. JS `||` tests falsiness of
481
+ // the *unwrapped* value, so when the inner type can be falsy the Some arm must
482
+ // re-test (`Some(0) || 1 === 1`); array/user inners are always truthy and
483
+ // unwrap directly. Mirrors narrow.ts's canBeFalsy gate.
392
484
  if (e.op === "||" && e.left.ty.kind === "optional") {
393
485
  const optExpr = lowerExpr(e.left, binds);
394
486
  const defaultExpr = lowerExpr(e.right, binds);
395
487
  const bound = matchBinder("value");
488
+ const truthy = valueTruthyCond({ kind: "var", name: bound }, e.left.ty.inner);
489
+ const someBody = truthy
490
+ ? { kind: "if", cond: truthy, then: { kind: "var", name: bound }, else: defaultExpr }
491
+ : { kind: "var", name: bound };
396
492
  return {
397
493
  kind: "match", scrutinee: optExpr,
398
494
  arms: [
399
- { pattern: `.some ${bound}`, body: { kind: "var", name: bound } },
495
+ { pattern: `.some ${bound}`, body: someBody },
400
496
  { pattern: ".none", body: defaultExpr },
401
497
  ],
402
498
  };
403
499
  }
404
- // || on map index → if key in map then map[key] else default
500
+ // || on map index → if key in map then map[key] else default. The stored
501
+ // value is still subject to JS falsiness (`counts.get(k) || 1` returns 1 when
502
+ // the stored value is 0), so for falsy-capable value types the present branch
503
+ // re-tests the value too. Always-truthy value types unwrap directly.
405
504
  if (e.op === "||" && e.left.kind === "index" && e.left.obj.ty.kind === "map") {
406
505
  const map = lowerExpr(e.left.obj, binds);
407
506
  const key = lowerExpr(e.left.idx, binds);
408
507
  const right = lowerExpr(e.right, binds);
508
+ const got = { kind: "index", arr: map, idx: key };
509
+ const truthy = valueTruthyCond(got, e.left.obj.ty.value);
409
510
  return {
410
511
  kind: "if",
411
512
  cond: { kind: "binop", op: "in", left: key, right: map },
412
- then: { kind: "index", arr: map, idx: key }, else: right,
513
+ then: truthy ? { kind: "if", cond: truthy, then: got, else: right } : got,
514
+ else: right,
413
515
  };
414
516
  }
415
- // || on non-optional string/array/userif non-empty then x else default
416
- if (e.op === "||" && (e.left.ty.kind === "string" || e.left.ty.kind === "array" ||
517
+ // || on non-optional array → `xs` itself: every array (even `[]`) is truthy
518
+ // in JS, so `xs || ys` short-circuits to `xs` and `ys` is never evaluated.
519
+ // resolve types the whole `||` as the array, so any optional context (e.g.
520
+ // `xs || undefined`) gets its single Some-wrap from the standard coercion at
521
+ // the use site — this rule must not add one. Mirrors the `!array` rule above.
522
+ if (e.op === "||" && e.left.ty.kind === "array") {
523
+ return lowerExpr(e.left, binds);
524
+ }
525
+ // || on non-optional string/user → if non-empty then x else default
526
+ if (e.op === "||" && (e.left.ty.kind === "string" ||
417
527
  (e.left.ty.kind === "user" && e.right.ty.kind === "string"))) {
418
528
  const left = lowerExpr(e.left, binds);
419
529
  const right = lowerExpr(e.right, binds);
@@ -421,8 +531,8 @@ function lowerExpr(e, binds) {
421
531
  const rightIsUndef = e.right.kind === "var" && e.right.name === "undefined";
422
532
  return {
423
533
  kind: "if",
424
- // strings carry the `length` marker, arrays `size` both render to `|x|`
425
- // in Dafny, but Lean's String has no `.size` field (it's `.length`).
534
+ // strings carry the `length` marker — it renders to `|x|` in Dafny and
535
+ // `.length` in Lean (whose String has no `.size` field).
426
536
  cond: { kind: "binop", op: ">", left: { kind: "field", obj: left, field: e.left.ty.kind === "string" ? "length" : "size" }, right: { kind: "num", value: 0 } },
427
537
  then: rightIsUndef ? { kind: "app", fn: "Some", args: [left] } : left,
428
538
  else: right,
@@ -442,20 +552,43 @@ function lowerExpr(e, binds) {
442
552
  else: { kind: "var", name: "undefined" },
443
553
  };
444
554
  }
445
- // int + string NatToString(int) + string (string concatenation)
446
- if (e.op === "+" && _opts.backend === "dafny") {
447
- const isIntL = e.left.ty.kind === "int" || e.left.ty.kind === "nat";
448
- const isIntR = e.right.ty.kind === "int" || e.right.ty.kind === "nat";
449
- if (isIntL && e.right.ty.kind === "string") {
450
- return { kind: "binop", op: "+",
451
- left: { kind: "app", fn: "NatToString", args: [lowerExpr(e.left, binds)] },
452
- right: lowerExpr(e.right, binds) };
453
- }
454
- if (e.left.ty.kind === "string" && isIntR) {
455
- return { kind: "binop", op: "+",
456
- left: lowerExpr(e.left, binds),
457
- right: { kind: "app", fn: "NatToString", args: [lowerExpr(e.right, binds)] } };
458
- }
555
+ // String concatenation: `+` with a string operand. Stringify int/nat
556
+ // operands (Dafny NatToString, Lean toString) and join with arrayConcat
557
+ // (rendered `+` in Dafny, `++` in Lean).
558
+ if (e.op === "+" && (e.left.ty.kind === "string" || e.right.ty.kind === "string")) {
559
+ const strify = (o) => {
560
+ if (o.ty.kind !== "int" && o.ty.kind !== "nat")
561
+ return lowerExpr(o, binds);
562
+ // Lean `toString` handles any Int; Dafny needs IntToString for signed
563
+ // ints (NatToString is nat-only).
564
+ const fn = _opts.backend !== "dafny" ? "ToString" : o.ty.kind === "nat" ? "NatToString" : "IntToString";
565
+ return { kind: "app", fn, args: [lowerExpr(o, binds)] };
566
+ };
567
+ return { kind: "binop", op: "arrayConcat", left: strify(e.left), right: strify(e.right) };
568
+ }
569
+ // JS `%` is truncated (sign of the dividend); a signed `int` differs from the
570
+ // Euclidean `%` of Dafny/Lean, so route it through JSRem (Lean: `Int.tmod`).
571
+ if (e.op === "%" && e.left.ty.kind === "int") {
572
+ return { kind: "app", fn: "JSRem", args: [lowerExpr(e.left, binds), lowerExpr(e.right, binds)] };
573
+ }
574
+ // JS bigint `/` truncates toward zero (`-3n / 2n === -1n`); it differs from the
575
+ // floored `/` of Dafny/Lean, so route it through JSTruncDiv (Lean: `Int.tdiv`).
576
+ if (e.op === "/" && e.ty.kind === "int") {
577
+ return { kind: "app", fn: "JSTruncDiv", args: [lowerExpr(e.left, binds), lowerExpr(e.right, binds)] };
578
+ }
579
+ // JS string ordering is lexicographic vs Dafny's seq prefix order, so route
580
+ // through JSStringLt. Dafny-only: Lean's native `<` is already lexicographic.
581
+ if (_opts.backend === "dafny" && ["<", "<=", ">", ">="].includes(e.op) && e.left.ty.kind === "string") {
582
+ const l = lowerExpr(e.left, binds), r = lowerExpr(e.right, binds);
583
+ const lt = (x, y) => ({ kind: "app", fn: "JSStringLt", args: [x, y] });
584
+ const not = (x) => ({ kind: "unop", op: "¬", expr: x });
585
+ if (e.op === "<")
586
+ return lt(l, r);
587
+ if (e.op === ">")
588
+ return lt(r, l);
589
+ if (e.op === "<=")
590
+ return not(lt(r, l));
591
+ return not(lt(l, r)); // >=
459
592
  }
460
593
  // Numeric int→real coercion. After resolve, `/` is always real, and any
461
594
  // arithmetic/comparison mixing real and integral operands is real-valued.
@@ -571,14 +704,21 @@ function lowerExpr(e, binds) {
571
704
  let method = e.fn.field;
572
705
  const args = e.args.map((a, i) => {
573
706
  const lowered = lowerExpr(a, binds);
574
- // arr.with index (first arg) needs .toNat when Int-typed
575
- if (e.fn.kind === "field" && e.fn.field === "with" && e.fn.obj.ty.kind === "array" && i === 0 && !isNat(a.ty))
707
+ // Array index args must be nat in Lean: `with`'s index (0), includes/indexOf `from` (1).
708
+ const isArrIdxArg = e.fn.kind === "field" && e.fn.obj.ty.kind === "array" &&
709
+ ((e.fn.field === "with" && i === 0) || ((e.fn.field === "includes" || e.fn.field === "indexOf") && i === 1));
710
+ if (isArrIdxArg && !isNat(a.ty))
576
711
  return { kind: "toNat", expr: lowered };
577
712
  return lowered;
578
713
  });
579
- // arr.concat(otherArr): array argument real concatenation, not push
580
- if (method === "concat" && e.fn.obj.ty.kind === "array" && e.args.length === 1 && e.args[0].ty.kind === "array") {
581
- return { kind: "binop", op: "arrayConcat", left: recv, right: args[0] };
714
+ // arr.concat(...args): each array arg is spread, each value arg appended.
715
+ if (method === "concat" && e.fn.obj.ty.kind === "array") {
716
+ let acc = recv;
717
+ for (let k = 0; k < args.length; k++) {
718
+ const piece = e.args[k].ty.kind === "array" ? args[k] : { kind: "arrayLiteral", elems: [args[k]] };
719
+ acc = { kind: "binop", op: "arrayConcat", left: acc, right: piece };
720
+ }
721
+ return acc;
582
722
  }
583
723
  // Spec-context map get: result type is non-optional → direct access
584
724
  if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
@@ -1212,18 +1352,24 @@ function transformStmt(s, typeDecls) {
1212
1352
  case "break": return [{ kind: "break" }];
1213
1353
  case "continue": return [{ kind: "continue" }];
1214
1354
  case "expr": {
1215
- // Mutating collection call: m.set(k, v) → m := m.set(k, v)
1216
- // Same for s.add(x) on sets, arr.push(x)
1217
- if (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
1218
- s.expr.fn.obj.kind === "var" &&
1219
- ((s.expr.fn.obj.ty.kind === "map" || s.expr.fn.obj.ty.kind === "set") &&
1220
- (s.expr.fn.field === "set" || s.expr.fn.field === "add" || s.expr.fn.field === "delete")) ||
1221
- (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
1222
- s.expr.fn.obj.kind === "var" && s.expr.fn.obj.ty.kind === "array" &&
1223
- s.expr.fn.field === "push")) {
1224
- const receiver = s.expr.fn.obj.name;
1225
- const { binds, expr } = liftMethodCalls(s.expr);
1226
- return [...binds, { kind: "assign", target: receiver, value: expr }];
1355
+ // Mutating collection call: m.set(k, v) → m := m.set(k, v) (same for set
1356
+ // .add/.delete and array .push). The receiver may be a bare var, or a
1357
+ // field path rooted at a var (b.items.push(v) b := b.(items := b.items + [v])).
1358
+ if (s.expr.kind === "call" && s.expr.fn.kind === "field") {
1359
+ const recv = s.expr.fn.obj;
1360
+ const f = s.expr.fn.field;
1361
+ const isMutating = ((recv.ty.kind === "map" || recv.ty.kind === "set") && (f === "set" || f === "add" || f === "delete")) ||
1362
+ (recv.ty.kind === "array" && f === "push");
1363
+ if (isMutating && recv.kind === "var") {
1364
+ const { binds, expr } = liftMethodCalls(s.expr);
1365
+ return [...binds, { kind: "assign", target: recv.name, value: expr }];
1366
+ }
1367
+ if (isMutating && recv.kind === "field") {
1368
+ const { binds, expr } = liftMethodCalls(s.expr);
1369
+ const upd = buildNestedFieldUpdate(recv, expr);
1370
+ if (upd)
1371
+ return [...binds, { kind: "assign", target: upd.root, value: upd.value }];
1372
+ }
1227
1373
  }
1228
1374
  // Optional chaining on map.get at statement level: m.get(k)?.push(v)
1229
1375
  // → if k in m { m[k] := m[k] + [v] } (actual mutation, not value-discard).
@@ -1826,7 +1972,7 @@ export function transformModule(mod, specImport) {
1826
1972
  else if (fn.forcePure) {
1827
1973
  // //@ pure but body can't be auto-converted — emit function by method
1828
1974
  _forofCounters.clear();
1829
- const methodBody = transformStmts(fn.body, mod.typeDecls);
1975
+ const methodBody = promoteAssignedLets(transformStmts(fn.body, mod.typeDecls));
1830
1976
  defByMethods.push({
1831
1977
  kind: "def-by-method",
1832
1978
  name: fn.name,
@@ -1888,7 +2034,7 @@ export function transformModule(mod, specImport) {
1888
2034
  _forofCounters.clear();
1889
2035
  let body = pureDefNames.has(fn.name)
1890
2036
  ? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
1891
- : transformStmts(fn.body, mod.typeDecls);
2037
+ : promoteAssignedLets(transformStmts(fn.body, mod.typeDecls));
1892
2038
  // Shadow reassigned parameters with mutable locals
1893
2039
  const paramNames = new Set(fn.params.map(p => p.name));
1894
2040
  const reassigned = findReassignedNames(fn.body, paramNames);
@@ -1915,7 +2061,7 @@ export function transformModule(mod, specImport) {
1915
2061
  const classMethods = cls.methods.map(fn => {
1916
2062
  const ensures = fn.ensures.map(transformExpr);
1917
2063
  _forofCounters.clear();
1918
- const body = transformStmts(fn.body, mod.typeDecls);
2064
+ const body = promoteAssignedLets(transformStmts(fn.body, mod.typeDecls));
1919
2065
  return {
1920
2066
  kind: "method",
1921
2067
  name: fn.name,
@@ -1,253 +0,0 @@
1
- /**
2
- * Lean IR → text. Trivial pretty-printer.
3
- * No logic, no type decisions — just serialization.
4
- */
5
- // ── Lean keyword escaping ────────────────────────────────────
6
- const LEAN_KEYWORDS = new Set([
7
- "def", "theorem", "lemma", "example", "structure", "class", "instance",
8
- "inductive", "where", "match", "with", "if", "then", "else", "do",
9
- "let", "mut", "return", "for", "in", "while", "break", "continue",
10
- "import", "open", "section", "namespace", "end", "set_option",
11
- "variable", "axiom", "constant", "private", "protected", "noncomputable",
12
- "partial", "unsafe", "macro", "syntax", "by", "fun", "have", "show",
13
- "at", "from", "to", "deriving", "extends", "true", "false",
14
- ]);
15
- function escapeName(name) {
16
- return LEAN_KEYWORDS.has(name) ? `«${name}»` : name;
17
- }
18
- // ── Operator precedence (for parenthesization) ──────────────
19
- const PREC = {
20
- "→": 1, "∨": 2, "∧": 3,
21
- "=": 4, "≠": 4, "≥": 4, "≤": 4, ">": 4, "<": 4,
22
- "+": 5, "-": 5, "*": 6, "/": 6, "%": 6,
23
- };
24
- function prec(op) { return PREC[op] ?? 10; }
25
- // ── Expression emission ─────────────────────────────────────
26
- function emitExpr(e, parentPrec) {
27
- switch (e.kind) {
28
- case "var": return escapeName(e.name);
29
- case "num": return `${e.value}`;
30
- case "bool": return e.value ? "true" : "false";
31
- case "str": return `"${e.value}"`;
32
- case "constructor": return `.${e.name}`;
33
- case "arrayLiteral":
34
- if (e.elems.length === 0)
35
- return `#[]`;
36
- return `#[${e.elems.map(el => emitExpr(el)).join(", ")}]`;
37
- case "dotCall": {
38
- const obj = emitExpr(e.obj);
39
- const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "dotCall";
40
- const receiver = wrap ? `(${obj})` : obj;
41
- const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
42
- return args.length > 0 ? `${receiver}.${e.method} ${args.join(" ")}` : `${receiver}.${e.method}`;
43
- }
44
- case "lambda": {
45
- const params = e.params.map(p => p.name).join(" ");
46
- // Single return statement → expression lambda
47
- if (e.body.length === 1 && e.body[0].kind === "return") {
48
- return `(fun ${params} => ${emitExpr(e.body[0].value)})`;
49
- }
50
- // Multi-statement → do block
51
- return `(fun ${params} => do\n${emitStmts(e.body, 2)})`;
52
- }
53
- case "unop":
54
- if (e.op === "¬")
55
- return `¬(${emitExpr(e.expr)})`;
56
- if (e.op === "-" && e.expr.kind === "num")
57
- return `-${e.expr.value}`;
58
- return `(-${emitExpr(e.expr)})`;
59
- case "binop": {
60
- const s = `${emitExpr(e.left, prec(e.op))} ${e.op} ${emitExpr(e.right, prec(e.op))}`;
61
- return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
62
- }
63
- case "implies": {
64
- const parts = [...e.premises.map(p => emitExpr(p)), emitExpr(e.conclusion)];
65
- const s = parts.join(" → ");
66
- return parentPrec !== undefined ? `(${s})` : s;
67
- }
68
- case "app": {
69
- const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
70
- return `${e.fn} ${args.join(" ")}`;
71
- }
72
- case "field": {
73
- const obj = emitExpr(e.obj);
74
- const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bool";
75
- return wrap ? `(${obj}).${escapeName(e.field)}` : `${obj}.${escapeName(e.field)}`;
76
- }
77
- case "toNat": {
78
- const inner = emitExpr(e.expr);
79
- const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
80
- return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
81
- }
82
- case "index":
83
- return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
84
- case "record": {
85
- const fields = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
86
- if (e.spread)
87
- return `{ ${emitExpr(e.spread)} with ${fields.join(", ")} }`;
88
- return `{ ${fields.join(", ")} }`;
89
- }
90
- case "if":
91
- return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
92
- case "match": {
93
- const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
94
- return `match ${e.scrutinee} with ${arms.join(" ")}`;
95
- }
96
- case "forall": return `∀ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
97
- case "exists": return `∃ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
98
- case "let": return `let ${e.name} := ${emitExpr(e.value)}\n${emitExpr(e.body)}`;
99
- }
100
- }
101
- // ── Statement emission ──────────────────────────────────────
102
- function emitStmts(stmts, indent) {
103
- const pad = " ".repeat(indent);
104
- return stmts.map(s => emitStmt(s, indent)).join("\n");
105
- }
106
- function emitStmt(s, indent) {
107
- const pad = " ".repeat(indent);
108
- switch (s.kind) {
109
- case "let":
110
- return s.mutable
111
- ? `${pad}let mut ${escapeName(s.name)} : ${s.type} := ${emitExpr(s.value)}`
112
- : `${pad}let ${escapeName(s.name)} := ${emitExpr(s.value)}`;
113
- case "assign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
114
- case "bind": return `${pad}${escapeName(s.target)} ← ${emitExpr(s.value)}`;
115
- case "let-bind": return `${pad}let ${s.name} ← ${emitExpr(s.value)}`;
116
- case "return": return `${pad}return ${emitExpr(s.value)}`;
117
- case "break": return `${pad}break`;
118
- case "continue": return `${pad}continue`;
119
- case "if": {
120
- let out = `${pad}if ${emitExpr(s.cond)} then\n${emitStmts(s.then, indent + 1)}`;
121
- if (s.else.length > 0) {
122
- if (s.else.length === 1 && s.else[0].kind === "if") {
123
- const ei = s.else[0];
124
- out += `\n${pad}else if ${emitExpr(ei.cond)} then\n${emitStmts(ei.then, indent + 1)}`;
125
- if (ei.else.length > 0)
126
- out += `\n${pad}else\n${emitStmts(ei.else, indent + 1)}`;
127
- }
128
- else {
129
- out += `\n${pad}else\n${emitStmts(s.else, indent + 1)}`;
130
- }
131
- }
132
- return out;
133
- }
134
- case "match": {
135
- const lines = [`${pad}match ${s.scrutinee} with`];
136
- for (const arm of s.arms) {
137
- lines.push(`${pad}| ${arm.pattern} =>`);
138
- lines.push(emitStmts(arm.body, indent + 1));
139
- }
140
- return lines.join("\n");
141
- }
142
- case "while": {
143
- const lines = [`${pad}while ${emitExpr(s.cond)}`];
144
- for (const inv of s.invariants)
145
- lines.push(`${pad} invariant ${emitExpr(inv)}`);
146
- if (s.doneWith)
147
- lines.push(`${pad} done_with ${emitExpr(s.doneWith)}`);
148
- if (s.decreasing)
149
- lines.push(`${pad} decreasing ${emitExpr(s.decreasing)}`);
150
- lines.push(`${pad}do`);
151
- lines.push(emitStmts(s.body, indent + 1));
152
- return lines.join("\n");
153
- }
154
- case "forin": {
155
- const lines = [`${pad}for ${s.idx} in [:${emitExpr(s.bound)}]`];
156
- for (const inv of s.invariants)
157
- lines.push(`${pad} invariant ${emitExpr(inv)}`);
158
- lines.push(`${pad}do`);
159
- lines.push(emitStmts(s.body, indent + 1));
160
- return lines.join("\n");
161
- }
162
- }
163
- }
164
- // ── Declaration emission ─────────────────────────────────────
165
- function emitDecl(d) {
166
- switch (d.kind) {
167
- case "inductive": {
168
- const lines = [`inductive ${d.name} where`];
169
- for (const c of d.constructors) {
170
- if (c.fields.length === 0) {
171
- lines.push(` | ${c.name} : ${d.name}`);
172
- }
173
- else {
174
- const params = c.fields.map(f => `(${escapeName(f.name)} : ${f.type})`).join(" ");
175
- lines.push(` | ${c.name} ${params} : ${d.name}`);
176
- }
177
- }
178
- if (d.deriving.length > 0)
179
- lines.push(`deriving ${d.deriving.join(", ")}`);
180
- return lines.join("\n");
181
- }
182
- case "structure": {
183
- const lines = [`structure ${d.name} where`];
184
- for (const f of d.fields)
185
- lines.push(` ${escapeName(f.name)} : ${f.type}`);
186
- if (d.deriving.length > 0)
187
- lines.push(`deriving ${d.deriving.join(", ")}`);
188
- return lines.join("\n");
189
- }
190
- case "def": {
191
- const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
192
- return `def ${d.name} ${params} : ${d.returnType} :=\n${emitPureExpr(d.body, 1)}`;
193
- }
194
- case "method": {
195
- const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
196
- const lines = [`method ${d.name} ${params} return (res : ${d.returnType})`];
197
- for (const r of d.requires)
198
- lines.push(` require ${emitExpr(r)}`);
199
- for (const e of d.ensures)
200
- lines.push(` ensures ${emitExpr(e)}`);
201
- lines.push(" do");
202
- lines.push(emitStmts(d.body, 2));
203
- return lines.join("\n");
204
- }
205
- case "namespace": {
206
- const lines = [`namespace ${d.name}`];
207
- for (const inner of d.decls)
208
- lines.push("", emitDecl(inner));
209
- lines.push("", `end ${d.name}`);
210
- return lines.join("\n");
211
- }
212
- }
213
- }
214
- /** Emit a pure expression with indented if/match blocks. */
215
- function emitPureExpr(e, indent) {
216
- const pad = " ".repeat(indent);
217
- switch (e.kind) {
218
- case "if":
219
- return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
220
- case "match": {
221
- const lines = [`${pad}match ${e.scrutinee} with`];
222
- for (const arm of e.arms) {
223
- lines.push(`${pad}| ${arm.pattern} =>`);
224
- lines.push(emitPureExpr(arm.body, indent + 1));
225
- }
226
- return lines.join("\n");
227
- }
228
- case "let":
229
- return `${pad}let ${e.name} := ${emitExpr(e.value)}\n${emitPureExpr(e.body, indent)}`;
230
- default:
231
- return `${pad}${emitExpr(e)}`;
232
- }
233
- }
234
- // ── File emission ────────────────────────────────────────────
235
- export function emitFile(file) {
236
- const lines = [];
237
- if (file.comment) {
238
- lines.push("/-");
239
- lines.push(file.comment);
240
- lines.push("-/");
241
- }
242
- for (const imp of file.imports)
243
- lines.push(`import ${imp}`);
244
- if (file.options.length > 0)
245
- lines.push("");
246
- for (const opt of file.options)
247
- lines.push(`set_option ${opt.key} ${opt.value}`);
248
- for (const decl of file.decls) {
249
- lines.push("");
250
- lines.push(emitDecl(decl));
251
- }
252
- return lines.join("\n") + "\n";
253
- }