lemmascript 0.3.1 → 0.3.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.
package/README.md CHANGED
@@ -13,6 +13,7 @@ Each example and case study is verified in Lean 4 and/or Dafny from the same ann
13
13
  See the internal [examples](examples).
14
14
 
15
15
  See the external case studies:
16
+ - **[collab-todo-lemmascript](https://github.com/midspiral/collab-todo-lemmascript/)** — collaborative task management web app (React + Supabase) with a verified domain model. Single `domain.ts` imported directly by the UI, hooks, and edge functions — no adapter layer. 123 Dafny lemmas (120 in a separate `domain.proofs.dfy`): 16-conjunct invariant preserved across 25 single-project + 3 cross-project actions, NoOp completeness/soundness, initialization. Dafny only.
16
17
  - **[colorwheel-lemmascript](https://github.com/midspiral/colorwheel-lemmascript/)** — verified color palette generator with mood + harmony constraints. 31 Lean proofs + 18 behavioral properties, 115 Dafny lemmas (invariant preservation, commutativity, NoOp completeness).
17
18
  - **[clear-split-lemmascript](https://github.com/midspiral/clear-split-lemmascript/)** — greenfield verified expense splitting web app. Conservation theorem, invariant preservation, delta laws — all proven in both Lean (no sorry) and Dafny (56 lemmas).
18
19
  - **[node-casbin-lemmascript](https://github.com/midspiral/node-casbin-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [node-casbin](https://github.com/casbin/node-casbin). 5 functions verified, 217 existing tests pass. End-to-end correctness and order independence for all 4 effect modes in both Lean and Dafny (39 lemmas).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemmascript",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,8 @@
12
12
  "scripts": {
13
13
  "build": "tsc -p tools/tsconfig.json",
14
14
  "prepublishOnly": "npm run build",
15
- "typecheck": "tsc -p tools/tsconfig.json --noEmit"
15
+ "typecheck": "tsc -p tools/tsconfig.json --noEmit",
16
+ "typecheck:examples": "tsc -p examples/tsconfig.json"
16
17
  },
17
18
  "dependencies": {
18
19
  "ts-morph": "^25.0.0"
@@ -94,6 +94,10 @@ function emitExpr(e) {
94
94
  return `${obj}[${args[0]} := ${args[1]}]`;
95
95
  if (e.method === "includes")
96
96
  return `(${args[0]} in ${obj})`;
97
+ if (e.method === "indexOf") {
98
+ needPreamble("SeqIndexOf");
99
+ return `SeqIndexOf(${obj}, ${args[0]})`;
100
+ }
97
101
  if (e.method === "push")
98
102
  return `(${obj} + [${args[0]}])`;
99
103
  if (e.method === "concat")
@@ -321,6 +325,8 @@ function emitExpr(e) {
321
325
  const vals = e.fields.map(f => emitExpr(f.value));
322
326
  return `${ctorName}(${vals.join(", ")})`;
323
327
  }
328
+ if (e.fields.length === 0)
329
+ return `map[]`;
324
330
  const vals = e.fields.map(f => emitExpr(f.value));
325
331
  return `(${vals.join(", ")})`;
326
332
  }
@@ -469,6 +475,8 @@ function emitDecl(d) {
469
475
  const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
470
476
  for (const r of d.requires)
471
477
  lines.push(` requires ${emitExpr(r)}`);
478
+ if (d.decreases)
479
+ lines.push(` decreases ${emitExpr(d.decreases)}`);
472
480
  lines.push(`{`);
473
481
  lines.push(emitPureExpr(d.body, 1));
474
482
  lines.push(`}`);
@@ -487,6 +495,20 @@ function emitDecl(d) {
487
495
  }
488
496
  return lines.join("\n");
489
497
  }
498
+ case "def-by-method": {
499
+ const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
500
+ const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
501
+ for (const r of d.requires)
502
+ lines.push(` requires ${emitExpr(r)}`);
503
+ if (d.decreases)
504
+ lines.push(` decreases ${emitExpr(d.decreases)}`);
505
+ lines.push(`{`);
506
+ lines.push(`}`);
507
+ lines.push(`by method {`);
508
+ lines.push(emitStmts(d.methodBody, 1));
509
+ lines.push(`}`);
510
+ return lines.join("\n");
511
+ }
490
512
  case "method": {
491
513
  const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
492
514
  const lines = [`method ${d.name}${tp}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
@@ -565,6 +587,25 @@ const CEIL_REAL = `function CeilReal(x: real): int
565
587
  if x == (x.Floor as real) then x.Floor
566
588
  else x.Floor + 1
567
589
  }`;
590
+ const SEQ_INDEX_OF = `function SeqIndexOf<T(==)>(s: seq<T>, x: T): int
591
+ ensures -1 <= SeqIndexOf(s, x) < |s|
592
+ ensures SeqIndexOf(s, x) >= 0 ==> s[SeqIndexOf(s, x)] == x
593
+ ensures SeqIndexOf(s, x) == -1 ==> x !in s
594
+ {
595
+ SeqIndexOfFrom(s, x, 0)
596
+ }
597
+
598
+ function SeqIndexOfFrom<T(==)>(s: seq<T>, x: T, from: nat): int
599
+ requires from <= |s|
600
+ ensures -1 <= SeqIndexOfFrom(s, x, from) < |s|
601
+ ensures SeqIndexOfFrom(s, x, from) >= 0 ==> s[SeqIndexOfFrom(s, x, from)] == x
602
+ ensures SeqIndexOfFrom(s, x, from) == -1 ==> forall i :: from <= i < |s| ==> s[i] != x
603
+ decreases |s| - from
604
+ {
605
+ if from == |s| then -1
606
+ else if s[from] == x then from as int
607
+ else SeqIndexOfFrom(s, x, from + 1)
608
+ }`;
568
609
  const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
569
610
  {
570
611
  StringIndexOfFrom(s, sub, 0)
@@ -633,6 +674,7 @@ const MATH_ABS = `function MathAbs(x: int): nat { if x >= 0 then x else -x }`;
633
674
  const SET_TO_SEQ = `method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
634
675
  ensures forall x :: x in s <==> x in res
635
676
  ensures |res| == |s|
677
+ ensures forall i, j :: 0 <= i < j < |res| ==> res[i] != res[j]
636
678
  {
637
679
  var remaining := s;
638
680
  res := [];
@@ -640,6 +682,7 @@ const SET_TO_SEQ = `method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
640
682
  invariant remaining <= s
641
683
  invariant forall x :: x in res <==> (x in s && x !in remaining)
642
684
  invariant |res| + |remaining| == |s|
685
+ invariant forall i, j :: 0 <= i < j < |res| ==> res[i] != res[j]
643
686
  decreases remaining
644
687
  {
645
688
  var x :| x in remaining;
@@ -656,6 +699,7 @@ const PREAMBLE_CODE = [
656
699
  ["JSFloorDiv", JS_FLOOR_DIV],
657
700
  ["CeilReal", CEIL_REAL],
658
701
  ["FloorReal", FLOOR_REAL],
702
+ ["SeqIndexOf", SEQ_INDEX_OF],
659
703
  ["StringIndexOf", STRING_INDEX_OF],
660
704
  ["StringTrim", STRING_TRIM],
661
705
  ["StringToLower", STRING_TO_LOWER],
@@ -736,26 +780,40 @@ function translatePattern(pattern) {
736
780
  export function emitDafnyFile(file, tsFileName) {
737
781
  buildRecordCtorMap(file.decls);
738
782
  _neededPreambles.clear();
739
- // Collect pure def names so we can skip their method wrappers
740
- const pureDefs = new Set();
741
- for (const d of file.decls) {
742
- if (d.kind === "namespace") {
743
- for (const inner of d.decls)
744
- if (inner.kind === "def")
745
- pureDefs.add(inner.name);
746
- }
747
- if (d.kind === "def")
748
- pureDefs.add(d.name);
749
- }
783
+ // Track successfully emitted pure defs method wrappers are only
784
+ // skipped when the corresponding pure def was actually emitted.
785
+ const emittedPureDefs = new Set();
750
786
  // Emit declarations
751
787
  const declLines = [];
752
788
  const skipped = [];
753
789
  for (const decl of file.decls) {
754
- if (decl.kind === "method" && pureDefs.has(decl.name))
790
+ if (decl.kind === "method" && emittedPureDefs.has(decl.name))
791
+ continue;
792
+ if (decl.kind === "namespace") {
793
+ // Emit each inner decl individually — if one fails, the rest survive
794
+ // and failed defs fall back to their method wrappers
795
+ for (const inner of decl.decls) {
796
+ try {
797
+ declLines.push("");
798
+ declLines.push(emitDecl(inner));
799
+ if (inner.kind === "def")
800
+ emittedPureDefs.add(inner.name);
801
+ }
802
+ catch (e) {
803
+ const name = "name" in inner ? inner.name : "unknown";
804
+ const msg = e.message;
805
+ console.error(`WARNING: skipping pure '${name}': ${msg}`);
806
+ declLines.push(`\n// LemmaScript: skipped pure ${name}`);
807
+ skipped.push(name);
808
+ }
809
+ }
755
810
  continue;
811
+ }
756
812
  try {
757
813
  declLines.push("");
758
814
  declLines.push(emitDecl(decl));
815
+ if (decl.kind === "def-by-method")
816
+ emittedPureDefs.add(decl.name);
759
817
  }
760
818
  catch (e) {
761
819
  const name = "name" in decl ? decl.name : "unknown";
@@ -8,6 +8,13 @@ import { Project, Node, SyntaxKind, ScriptTarget } from "ts-morph";
8
8
  // ── Expression extraction ────────────────────────────────────
9
9
  /** When set, calls whose function/method name matches this key are replaced with havoc. */
10
10
  let _havocKey = null;
11
+ /**
12
+ * Maps property-name fingerprints to type alias names for collapsed single-variant unions.
13
+ * TypeScript collapses `type X = | { kind: 'A'; ... }` to the underlying object type,
14
+ * causing getAliasSymbol() to return null. This map lets typeToString recover the alias.
15
+ * Populated in extractModule before type extraction; used by typeToString.
16
+ */
17
+ let _collapsedUnionMap = new Map();
11
18
  /** Generic bounds erasure map — set during extractFunction, applied in extractStmts. */
12
19
  let _typeParamMap = new Map();
13
20
  function _eraseGenerics(tsType) {
@@ -106,6 +113,14 @@ function extractExpr(node) {
106
113
  }
107
114
  // Call expression: f(a, b)
108
115
  if (Node.isCallExpression(node)) {
116
+ // Object.fromEntries(map) → identity (Map IS Record in Dafny)
117
+ const callee = node.getExpression();
118
+ if (Node.isPropertyAccessExpression(callee) &&
119
+ callee.getExpression().getText() === "Object" &&
120
+ callee.getName() === "fromEntries" &&
121
+ node.getArguments().length === 1) {
122
+ return extractExpr(node.getArguments()[0]);
123
+ }
109
124
  return {
110
125
  kind: "call",
111
126
  fn: extractExpr(node.getExpression()),
@@ -196,6 +211,7 @@ function extractExpr(node) {
196
211
  if (Node.isObjectLiteralExpression(node)) {
197
212
  let spread = null;
198
213
  const fields = [];
214
+ const computedFields = [];
199
215
  for (const prop of node.getProperties()) {
200
216
  if (Node.isSpreadAssignment(prop)) {
201
217
  spread = extractExpr(prop.getExpression());
@@ -205,10 +221,26 @@ function extractExpr(node) {
205
221
  fields.push({ name, value: { kind: "var", name } });
206
222
  }
207
223
  else if (Node.isPropertyAssignment(prop)) {
224
+ const nameNode = prop.getNameNode();
208
225
  const init = prop.getInitializer();
209
- if (init)
226
+ if (init && Node.isComputedPropertyName(nameNode)) {
227
+ computedFields.push({ key: extractExpr(nameNode.getExpression()), value: extractExpr(init) });
228
+ }
229
+ else if (init) {
210
230
  fields.push({ name: prop.getName(), value: extractExpr(init) });
231
+ }
232
+ }
233
+ }
234
+ // Desugar computed keys: { ...base, [k]: v } → base.set(k, v)
235
+ // No spread: { [k]: v } → {}.set(k, v) (empty map base)
236
+ if (computedFields.length > 0) {
237
+ let result = spread ?? { kind: "record", spread: null, fields: [] };
238
+ for (const cf of computedFields) {
239
+ result = { kind: "call",
240
+ fn: { kind: "field", obj: result, field: "set" },
241
+ args: [cf.key, cf.value] };
211
242
  }
243
+ return result;
212
244
  }
213
245
  return { kind: "record", spread, fields };
214
246
  }
@@ -234,8 +266,9 @@ function extractExpr(node) {
234
266
  if (name === "Map" && args && args.length === 1) {
235
267
  const argType = args[0].getType();
236
268
  const argSymbol = argType.getSymbol()?.getName() ?? argType.getAliasSymbol()?.getName();
237
- if (argSymbol === "Map") {
238
- // new Map(existingMap) identity (Dafny maps are value types)
269
+ const argTypeText = _eraseGenerics(typeToString(argType));
270
+ if (argSymbol === "Map" || argTypeText.startsWith("Record<")) {
271
+ // new Map(existingMap) or new Map(record) — identity (Dafny maps are value types)
239
272
  return extractExpr(args[0]);
240
273
  }
241
274
  // new Map(entries) — map-from-array constructor
@@ -257,6 +290,17 @@ function extractExpr(node) {
257
290
  if (Node.isAsExpression(node)) {
258
291
  return extractExpr(node.getExpression());
259
292
  }
293
+ // delete obj[key] → map delete expression
294
+ if (Node.isDeleteExpression(node)) {
295
+ const expr = node.getExpression();
296
+ if (Node.isElementAccessExpression(expr)) {
297
+ return {
298
+ kind: "call",
299
+ fn: { kind: "field", obj: extractExpr(expr.getExpression()), field: "delete" },
300
+ args: [extractExpr(expr.getArgumentExpression())],
301
+ };
302
+ }
303
+ }
260
304
  // null → undefined (both map to None in backends)
261
305
  if (Node.isNullLiteral(node)) {
262
306
  return { kind: "var", name: "undefined" };
@@ -289,6 +333,17 @@ function collectAnnotations(node, body) {
289
333
  return [...own, ...parseAnnotations(body[0])];
290
334
  return own;
291
335
  }
336
+ /** Check for bare `//@ pure` annotation (no expression). */
337
+ function hasPureAnnotation(node, body) {
338
+ const nodes = body && body.length > 0 ? [node, body[0]] : [node];
339
+ for (const n of nodes) {
340
+ for (const range of n.getLeadingCommentRanges()) {
341
+ if (range.getText().trim() === "//@ pure")
342
+ return true;
343
+ }
344
+ }
345
+ return false;
346
+ }
292
347
  // ── Type declaration extraction ──────────────────────────────
293
348
  function extractTypeDecl(decl, extraDecls) {
294
349
  const name = decl.getName();
@@ -320,6 +375,30 @@ function extractTypeDecl(decl, extraDecls) {
320
375
  }
321
376
  }
322
377
  }
378
+ // Single-variant discriminated union: type X = | { kind: 'Foo', ... }
379
+ // TypeScript collapses single-member unions to their member type,
380
+ // so type.isUnion() returns false. Detect by checking the source text
381
+ // for union syntax '|' AND a string-literal discriminant field.
382
+ if (type.isObject() && !type.isIntersection()) {
383
+ const srcText = decl.getTypeNode()?.getText() ?? "";
384
+ if (srcText.includes("|")) {
385
+ const disc = findDiscriminant([type]);
386
+ if (disc) {
387
+ const tagProp = type.getProperty(disc);
388
+ const tagType = tagProp?.getTypeAtLocation(decl);
389
+ const tag = tagType?.isStringLiteral() ? String(tagType.getLiteralValue()) : null;
390
+ if (tag) {
391
+ const fields = [];
392
+ for (const prop of type.getProperties()) {
393
+ if (prop.getName() === disc)
394
+ continue;
395
+ fields.push({ name: prop.getName(), tsType: typeToString(prop.getTypeAtLocation(decl)) });
396
+ }
397
+ return { name, typeParams: tpField, kind: "discriminated-union", discriminant: disc, variants: [{ name: tag, fields }] };
398
+ }
399
+ }
400
+ }
401
+ }
323
402
  if (type.isObject() || type.isIntersection())
324
403
  return extractRecord(name, type, decl, undefined, extraDecls);
325
404
  // Primitive type alias: type TaskId = number → alias
@@ -432,6 +511,13 @@ function typeToString(type) {
432
511
  const symbol = type.getSymbol() ?? type.getAliasSymbol();
433
512
  if (symbol) {
434
513
  const name = symbol.getName();
514
+ // Recover collapsed single-variant union alias via property fingerprint
515
+ if (name === "__type" && _collapsedUnionMap.size > 0) {
516
+ const props = type.getProperties().map(p => p.getName()).sort().join(",");
517
+ const alias = _collapsedUnionMap.get(props);
518
+ if (alias)
519
+ return alias;
520
+ }
435
521
  const typeArgs = type.getTypeArguments();
436
522
  if (typeArgs.length > 0) {
437
523
  return `${name}<${typeArgs.map(t => typeToString(t)).join(", ")}>`;
@@ -511,6 +597,38 @@ function extractStmts(stmts) {
511
597
  }
512
598
  continue;
513
599
  }
600
+ // Destructuring rest: const { [k]: _, ...rest } = map → let rest = map.delete(k)
601
+ if (!isHavoc && Node.isObjectBindingPattern(nameNode)) {
602
+ const elements = nameNode.getElements();
603
+ const restEl = elements.find(el => el.getDotDotDotToken());
604
+ const computedEls = elements.filter(el => {
605
+ const pn = el.getPropertyNameNode();
606
+ return pn && Node.isComputedPropertyName(pn);
607
+ });
608
+ if (restEl && computedEls.length > 0) {
609
+ const initializer = d.getInitializer();
610
+ if (initializer) {
611
+ let deleteInit = extractExpr(initializer);
612
+ for (const cel of computedEls) {
613
+ const pn = cel.getPropertyNameNode();
614
+ const keyExpr = extractExpr(pn.getExpression());
615
+ deleteInit = { kind: "call",
616
+ fn: { kind: "field", obj: deleteInit, field: "delete" },
617
+ args: [keyExpr] };
618
+ }
619
+ const declType = d.getType();
620
+ result.push({
621
+ kind: "let",
622
+ name: restEl.getName(),
623
+ mutable: s.getDeclarationKind() === "let",
624
+ tsType: _eraseGenerics(typeToString(declType)),
625
+ init: deleteInit,
626
+ line,
627
+ });
628
+ continue;
629
+ }
630
+ }
631
+ }
514
632
  const declType = d.getType();
515
633
  let init;
516
634
  if (isHavoc && !havocKey) {
@@ -571,12 +689,49 @@ function extractStmts(stmts) {
571
689
  else {
572
690
  names.push("_");
573
691
  }
692
+ // Unwrap Object.entries(expr) / Object.values(expr) to bare map iteration
693
+ let iterableExpr = s.getExpression();
694
+ if (Node.isCallExpression(iterableExpr)) {
695
+ const callee = iterableExpr.getExpression();
696
+ if (Node.isPropertyAccessExpression(callee) &&
697
+ callee.getExpression().getText() === "Object") {
698
+ const method = callee.getName();
699
+ if ((method === "entries" || method === "values") && iterableExpr.getArguments().length === 1) {
700
+ iterableExpr = iterableExpr.getArguments()[0];
701
+ // Object.values with single name → prepend "_" so it looks like [_, v] destructuring
702
+ if (method === "values" && names.length === 1) {
703
+ names.unshift("_");
704
+ }
705
+ }
706
+ }
707
+ }
574
708
  const bodyNode = s.getStatement();
575
709
  const bodyStmts = Node.isBlock(bodyNode) ? bodyNode.getStatements() : [bodyNode];
576
710
  const annots = collectAnnotations(s, bodyStmts);
577
711
  result.push({
578
712
  kind: "forof",
579
713
  names,
714
+ iterable: extractExpr(iterableExpr),
715
+ invariants: annots.filter(a => a.kind === "invariant").map(a => a.expr),
716
+ doneWith: annots.find(a => a.kind === "done_with")?.expr ?? null,
717
+ body: extractStmts(bodyStmts),
718
+ line,
719
+ });
720
+ continue;
721
+ }
722
+ // for...in: for (const k in obj) → treat as forof with single key name
723
+ if (Node.isForInStatement(s)) {
724
+ const init = s.getInitializer();
725
+ let name = "_";
726
+ if (Node.isVariableDeclarationList(init)) {
727
+ name = init.getDeclarations()[0]?.getName() ?? "_";
728
+ }
729
+ const bodyNode = s.getStatement();
730
+ const bodyStmts = Node.isBlock(bodyNode) ? bodyNode.getStatements() : [bodyNode];
731
+ const annots = collectAnnotations(s, bodyStmts);
732
+ result.push({
733
+ kind: "forof",
734
+ names: [name],
580
735
  iterable: extractExpr(s.getExpression()),
581
736
  invariants: annots.filter(a => a.kind === "invariant").map(a => a.expr),
582
737
  doneWith: annots.find(a => a.kind === "done_with")?.expr ?? null,
@@ -782,6 +937,8 @@ function extractFunction(fn, parentAnnotations) {
782
937
  })(),
783
938
  requires: annots.filter(a => a.kind === "requires").map(a => a.expr),
784
939
  ensures: annots.filter(a => a.kind === "ensures").map(a => a.expr),
940
+ decreases: annots.find(a => a.kind === "decreases")?.expr ?? null,
941
+ pure: hasPureAnnotation(fn, body && Node.isBlock(body) ? body.getStatements() : undefined),
785
942
  typeAnnotations,
786
943
  body: extractedBody,
787
944
  line: fn.getStartLineNumber(),
@@ -825,6 +982,22 @@ export function extractModule(sourceFile) {
825
982
  typeDecls.push({ name, kind: "record", fields });
826
983
  }
827
984
  }
985
+ // Pre-scan for collapsed single-variant unions so typeToString can recover alias names.
986
+ // TypeScript collapses `type X = | { kind: 'A'; ... }` to a plain object type, losing
987
+ // the alias. We record a fingerprint (sorted property names) → alias name mapping.
988
+ _collapsedUnionMap = new Map();
989
+ for (const stmt of sourceFile.getStatements()) {
990
+ if (Node.isTypeAliasDeclaration(stmt)) {
991
+ const type = stmt.getType();
992
+ if (!type.isUnion() && type.isObject() && !type.isIntersection()) {
993
+ const srcText = stmt.getTypeNode()?.getText() ?? "";
994
+ if (srcText.includes("|") && findDiscriminant([type])) {
995
+ const props = type.getProperties().map(p => p.getName()).sort().join(",");
996
+ _collapsedUnionMap.set(props, stmt.getName());
997
+ }
998
+ }
999
+ }
1000
+ }
828
1001
  // Extract type declarations in source order to respect dependencies
829
1002
  // Skip types already declared via //@ declare-type
830
1003
  const declaredNames = new Set(typeDecls.map(d => d.name));
@@ -1184,6 +1357,12 @@ export function extractModule(sourceFile) {
1184
1357
  }
1185
1358
  const sym = retType.getSymbol();
1186
1359
  if (sym?.getName() === "__type" && retType.isObject() && !retType.isArray()) {
1360
+ // Try typeToString first — it resolves collapsed single-variant unions
1361
+ const resolved = typeToString(retType);
1362
+ if (resolved !== "__type" && !resolved.includes("__type") && knownTypes.has(resolved)) {
1363
+ fn.returnType = resolved;
1364
+ continue;
1365
+ }
1187
1366
  const synName = fn.name.charAt(0).toUpperCase() + fn.name.slice(1) + "Result";
1188
1367
  if (!knownTypes.has(synName)) {
1189
1368
  const extra = [];
@@ -325,6 +325,8 @@ function emitDecl(d) {
325
325
  const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
326
326
  return `def ${d.name} ${params} : ${tyToLean(d.returnType)} :=\n${emitPureExpr(d.body, 1)}`;
327
327
  }
328
+ case "def-by-method":
329
+ throw new Error("function by method is not supported for Lean backend");
328
330
  case "method": {
329
331
  const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
330
332
  const lines = [`method ${d.name} ${params} return (res : ${tyToLean(d.returnType)})`];