lemmascript 0.3.3 → 0.5.0

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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `lsc info` — emit a JSON summary of verified functions in a TS file.
3
+ *
4
+ * Pipeline: extract only (no resolve/transform/emit). Walks Raw IR for
5
+ * top-level functions and class methods, preserving the original `//@ `
6
+ * source text for clauses (no specparser round-trip).
7
+ *
8
+ * Output: `foo.ts.json` next to `foo.ts`, with shape:
9
+ * { method: { sig, requires, ensures, decreases }, ... }
10
+ * Class methods key as `ClassName.method`.
11
+ */
12
+ import { writeFileSync } from "fs";
13
+ import { parseTsType, tyToCanonical } from "./types.js";
14
+ function renderSig(fn) {
15
+ const params = fn.params.map(p => `${p.name}: ${tyToCanonical(parseTsType(p.tsType))}`).join(", ");
16
+ return `(${params}): ${tyToCanonical(parseTsType(fn.returnType))}`;
17
+ }
18
+ function fnToInfo(fn) {
19
+ return {
20
+ sig: renderSig(fn),
21
+ requires: fn.requires,
22
+ ensures: fn.ensures,
23
+ decreases: fn.decreases,
24
+ };
25
+ }
26
+ export function runInfo(raw, outPath) {
27
+ const out = {};
28
+ for (const fn of raw.functions) {
29
+ out[fn.name] = fnToInfo(fn);
30
+ }
31
+ for (const cls of raw.classes) {
32
+ for (const m of cls.methods) {
33
+ out[`${cls.name}.${m.name}`] = fnToInfo(m);
34
+ }
35
+ }
36
+ writeFileSync(outPath, JSON.stringify(out, null, 2) + "\n");
37
+ console.log(`Wrote ${outPath}`);
38
+ }
@@ -31,6 +31,15 @@ function tyToLean(ty) {
31
31
  return inner.includes(" ") ? `Option (${inner})` : `Option ${inner}`;
32
32
  }
33
33
  case "user": return ty.name;
34
+ case "fn": {
35
+ const params = ty.params.map(p => {
36
+ const s = tyToLean(p);
37
+ return s.includes(" ") ? `(${s})` : s;
38
+ });
39
+ const ret = tyToLean(ty.result);
40
+ const retStr = ret.includes(" ") ? `(${ret})` : ret;
41
+ return [...params, retStr].join(" → ");
42
+ }
34
43
  case "unknown": return "_";
35
44
  }
36
45
  }
@@ -45,6 +54,10 @@ const LEAN_KEYWORDS = new Set([
45
54
  "at", "from", "to", "deriving", "extends", "true", "false",
46
55
  ]);
47
56
  function escapeName(name) {
57
+ // \result is carried through the IR as the var name "\\result"; render it
58
+ // as Lean's canonical return-value identifier (matches `return (res : T)`).
59
+ if (name === "\\result")
60
+ return "res";
48
61
  return LEAN_KEYWORDS.has(name) ? `«${name}»` : name;
49
62
  }
50
63
  // ── Operator precedence (for parenthesization) ──────────────
@@ -75,6 +88,8 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
75
88
  return `${obj}.set! ${args[0]} ${args[1]}`;
76
89
  if (method === "push")
77
90
  return `Array.push ${obj} ${args[0]}`;
91
+ if (method === "concat")
92
+ return `Array.push ${obj} ${args[0]}`;
78
93
  }
79
94
  // String methods
80
95
  if (tyKind === "string") {
@@ -107,13 +122,30 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
107
122
  throw new Error(`Unsupported Lean method call: .${method}() on ${tyKind}`);
108
123
  }
109
124
  // ── 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) {
129
+ const inner = emitExpr(sub, parentPrec);
130
+ return (sub.kind === "forall" || sub.kind === "exists") ? `(${inner})` : inner;
131
+ }
110
132
  function emitExpr(e, parentPrec) {
111
133
  switch (e.kind) {
112
134
  case "var": return escapeName(e.name);
113
135
  case "num": return `${e.value}`;
114
136
  case "bool": return e.value ? "true" : "false";
115
137
  case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
116
- case "constructor": return `.${e.name}`;
138
+ case "constructor": {
139
+ // With type: emit `Type.name` (unambiguous; needed in expression positions
140
+ // like `match ... | .none => Type.some x` where elaboration can't infer).
141
+ // Without type: emit `.name` (dotted form; works in pattern positions
142
+ // and where the expected type is clear from context).
143
+ const head = e.type ? `${e.type}.${e.name}` : `.${e.name}`;
144
+ if (!e.args || e.args.length === 0)
145
+ return head;
146
+ 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));
147
+ return `${head} ${args.join(" ")}`;
148
+ }
117
149
  case "arrayLiteral":
118
150
  if (e.elems.length === 0)
119
151
  return `#[]`;
@@ -143,12 +175,19 @@ function emitExpr(e, parentPrec) {
143
175
  return `-${e.expr.value}`;
144
176
  return `(-${emitExpr(e.expr)})`;
145
177
  case "binop": {
178
+ // `k in m` (map/set membership) → `m.contains k` in Lean. Dafny has
179
+ // native `in`; Lean uses the method form for HashMap/HashSet.
180
+ if (e.op === "in") {
181
+ const recv = emitExpr(e.right);
182
+ const wrap = e.right.kind === "binop" || e.right.kind === "app" || e.right.kind === "methodCall";
183
+ return `${wrap ? `(${recv})` : recv}.contains ${emitExpr(e.left)}`;
184
+ }
146
185
  const op = e.op === "arrayConcat" ? "++" : e.op;
147
- const s = `${emitExpr(e.left, prec(e.op))} ${op} ${emitExpr(e.right, prec(e.op))}`;
186
+ const s = `${wrapQuantifier(e.left, prec(e.op))} ${op} ${emitExpr(e.right, prec(e.op))}`;
148
187
  return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
149
188
  }
150
189
  case "implies": {
151
- const parts = [...e.premises.map(p => emitExpr(p)), emitExpr(e.conclusion)];
190
+ const parts = [...e.premises.map(p => wrapQuantifier(p)), emitExpr(e.conclusion)];
152
191
  const s = parts.join(" → ");
153
192
  return parentPrec !== undefined ? `(${s})` : s;
154
193
  }
@@ -182,8 +221,12 @@ function emitExpr(e, parentPrec) {
182
221
  case "if":
183
222
  return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
184
223
  case "match": {
224
+ // Always parenthesize inline matches — Lean parses alternatives greedily,
225
+ // so any token after an arm body (`→`, another match's `|`, etc.) would
226
+ // bleed into the last `.none` case without explicit bracketing.
185
227
  const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
186
- return `match ${typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee)} with ${arms.join(" ")}`;
228
+ const scrut = typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee);
229
+ return `(match ${scrut} with ${arms.join(" ")})`;
187
230
  }
188
231
  case "forall": return `∀ ${e.var} : ${tyToLean(e.type)}, ${emitExpr(e.body)}`;
189
232
  case "exists": return `∃ ${e.var} : ${tyToLean(e.type)}, ${emitExpr(e.body)}`;
@@ -191,6 +234,24 @@ function emitExpr(e, parentPrec) {
191
234
  default: throw new Error(`Unsupported Lean expression: ${e.kind}`);
192
235
  }
193
236
  }
237
+ /** True if the IR expression emits to a Prop-valued Lean term. `transformExpr`
238
+ * routes TS comparisons/logicals through `OP_MAP` (`===`→`=`, `&&`→`∧`, `!`→`¬`)
239
+ * so these top-level ops land in Prop. `in` stays Bool (emits `.contains`), and
240
+ * bare method calls / vars / field accesses remain at their declared type. */
241
+ function isPropValued(e) {
242
+ switch (e.kind) {
243
+ case "binop":
244
+ return ["=", "≠", "≥", "≤", ">", "<", "∧", "∨"].includes(e.op);
245
+ case "unop":
246
+ return e.op === "¬";
247
+ case "implies":
248
+ case "forall":
249
+ case "exists":
250
+ return true;
251
+ default:
252
+ return false;
253
+ }
254
+ }
194
255
  // ── Statement emission ──────────────────────────────────────
195
256
  function emitStmts(stmts, indent) {
196
257
  const pad = " ".repeat(indent);
@@ -207,7 +268,18 @@ function emitStmt(s, indent) {
207
268
  case "ghostLet":
208
269
  return `${pad}let mut ${escapeName(s.name)} : ${tyToLean(s.type)} := ${emitExpr(s.value)}`;
209
270
  case "ghostAssign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
210
- case "assert": return `${pad}assertGadget (${emitExpr(s.expr)})`;
271
+ case "assert": {
272
+ if (s.assumed)
273
+ throw new Error("//@ assume: not supported in Lean backend.");
274
+ // WPGen.assert needs a Prop; bare Bool expressions (`k in m` → `.contains`,
275
+ // method calls, vars) lack a matching WPGen instance and silently fall back
276
+ // to WPGen.default, which drops the assertion. Coerce to Prop via `= true`.
277
+ // Top-level Prop constructs (`=`, `<`, `∧`, `¬`, `∀`, `∃`, `→`) already
278
+ // land in Prop — Lean auto-coerces inner Bools there.
279
+ const inner = emitExpr(s.expr);
280
+ const wrapped = isPropValued(s.expr) ? inner : `(${inner}) = true`;
281
+ return `${pad}assertGadget (${wrapped})`;
282
+ }
211
283
  case "bind": return `${pad}${escapeName(s.target)} ← ${emitExpr(s.value)}`;
212
284
  case "let-bind": return `${pad}let ${s.name} ← ${emitExpr(s.value)}`;
213
285
  case "return": return `${pad}return ${emitExpr(s.value)}`;
@@ -349,6 +421,10 @@ function emitDecl(d) {
349
421
  throw new Error(`Lean class support not yet implemented: ${d.name}`);
350
422
  case "const":
351
423
  return `def ${escapeName(d.name)} : ${tyToLean(d.type)} := ${emitExpr(d.value)}`;
424
+ case "extern":
425
+ // Lean: emit an opaque function declaration. The user is expected to
426
+ // provide an axiomatic body or a stub in the companion spec file.
427
+ throw new Error(`Lean extern support not yet implemented: ${d.name}`);
352
428
  }
353
429
  }
354
430
  /** Emit a pure expression with indented if/match blocks. */
package/tools/dist/lsc.js CHANGED
@@ -2,18 +2,21 @@
2
2
  /**
3
3
  * lsc — LemmaScript compiler CLI
4
4
  *
5
- * Pipeline: extract → resolve → transform → emit
5
+ * Pipeline: extract → resolve → narrow → transform → peephole → emit
6
6
  */
7
7
  import { Project, ScriptTarget } from "ts-morph";
8
8
  import { existsSync } from "fs";
9
9
  import path from "path";
10
10
  import { extractModule } from "./extract.js";
11
11
  import { resolveModule } from "./resolve.js";
12
+ import { narrowModule } from "./narrow.js";
12
13
  import { transformModuleLean, transformModuleDafny } from "./transform.js";
14
+ import { peepholeModule } from "./peephole.js";
13
15
  import { emitLeanFile } from "./lean-emit.js";
14
16
  import { emitDafnyFile } from "./dafny-emit.js";
15
17
  import { dafnyGen, dafnyCheckDiff, dafnyVerify, dafnyRegen } from "./dafny-commands.js";
16
18
  import { leanGen, leanCheck } from "./lean-commands.js";
19
+ import { runInfo } from "./info-command.js";
17
20
  function main() {
18
21
  const args = process.argv.slice(2);
19
22
  const backendIdx = args.findIndex(a => a.startsWith("--backend="));
@@ -41,7 +44,7 @@ function main() {
41
44
  }
42
45
  const [cmd, filePath] = args;
43
46
  if (!cmd || !filePath) {
44
- console.error("Usage: lsc <gen|check|regen|extract> [--backend=lean|dafny] <file.ts>");
47
+ console.error("Usage: lsc <gen|check|regen|extract|info> [--backend=lean|dafny] <file.ts>");
45
48
  process.exit(1);
46
49
  }
47
50
  const absPath = path.resolve(filePath);
@@ -68,28 +71,42 @@ function main() {
68
71
  : new Project({ compilerOptions: { strict: true, target: ScriptTarget.ESNext, lib: ["lib.esnext.d.ts"] } });
69
72
  const sourceFile = project.addSourceFileAtPath(absPath);
70
73
  project.resolveSourceFileDependencies();
71
- // Check //@ backend directive — skip if backend doesn't match
72
- const backendDirective = sourceFile.getFullText().match(/\/\/@ backend (\w+)/);
73
- if (backendDirective && backendDirective[1] !== backend) {
74
+ const fullText = sourceFile.getFullText();
75
+ // Check //@ backend directive — skip if backend doesn't match.
76
+ // `extract` and `info` are backend-neutral and always run.
77
+ const backendDirective = fullText.match(/\/\/@ backend (\w+)/);
78
+ if (cmd !== "extract" && cmd !== "info" && backendDirective && backendDirective[1] !== backend) {
74
79
  console.log(`Skipped: ${path.basename(filePath)} (//@ backend ${backendDirective[1]}, current: ${backend})`);
75
80
  return;
76
81
  }
82
+ // File-level directives consumed by the Dafny emitter.
83
+ const safeSlice = /\/\/@ safe-slice\b/.test(fullText);
77
84
  // Extract: ts-morph → Raw IR
78
85
  const raw = extractModule(sourceFile);
79
86
  if (cmd === "extract") {
80
87
  console.log(JSON.stringify(raw, null, 2));
81
88
  return;
82
89
  }
90
+ if (cmd === "info") {
91
+ const outPath = path.join(path.dirname(absPath), `${path.basename(filePath, ".ts")}.ts.json`);
92
+ runInfo(raw, outPath);
93
+ return;
94
+ }
83
95
  // Resolve: Raw IR → Typed IR
84
- const typed = resolveModule(raw);
96
+ const resolved = resolveModule(raw);
97
+ // Narrow: Typed IR → Typed IR (rewrites optional-narrowing patterns to someMatch)
98
+ const typed = narrowModule(resolved);
85
99
  const dir = path.dirname(absPath);
86
100
  const base = path.basename(filePath, ".ts");
87
101
  // ── Dafny backend ─────────────────────────────────────────
88
102
  if (backend === "dafny") {
89
- const { typesFile, defFile } = transformModuleDafny(typed);
103
+ let { typesFile, defFile } = transformModuleDafny(typed);
104
+ if (typesFile)
105
+ typesFile = peepholeModule(typesFile, "dafny");
106
+ defFile = peepholeModule(defFile, "dafny");
90
107
  const allDecls = [...(typesFile?.decls ?? []), ...defFile.decls];
91
108
  const merged = { ...defFile, decls: allDecls };
92
- const text = emitDafnyFile(merged, path.basename(filePath));
109
+ const text = emitDafnyFile(merged, path.basename(filePath), { safeSlice });
93
110
  const genPath = path.join(dir, `${base}.dfy.gen`);
94
111
  const dfyPath = path.join(dir, `${base}.dfy`);
95
112
  const basePath = path.join(dir, `${base}.dfy.base`);
@@ -121,7 +138,10 @@ function main() {
121
138
  // ── Lean backend ──────────────────────────────────────────
122
139
  const specPath = path.join(dir, `${base}.spec.lean`);
123
140
  const specImport = existsSync(specPath) ? `«${base}.spec»` : undefined;
124
- const { typesFile, defFile } = transformModuleLean(typed, specImport);
141
+ let { typesFile, defFile } = transformModuleLean(typed, specImport);
142
+ if (typesFile)
143
+ typesFile = peepholeModule(typesFile, "lean");
144
+ defFile = peepholeModule(defFile, "lean");
125
145
  const typesPath = typesFile ? path.join(dir, `${base}.types.lean`) : null;
126
146
  const typesText = typesFile ? emitLeanFile(typesFile) : null;
127
147
  const defPath = path.join(dir, `${base}.def.lean`);