lemmascript 0.5.18 → 0.5.20

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.
@@ -5,7 +5,7 @@
5
5
  * and support-import selection.
6
6
  */
7
7
  import { anyExpr, usesNameInDecl, patternBinders } from "./ir.js";
8
- import { freshName } from "./names.js";
8
+ import { freshNameWhere } from "./names.js";
9
9
  // ── Ty → Lean type string ──────────────────────────────────
10
10
  function tyToLean(ty) {
11
11
  switch (ty.kind) {
@@ -190,9 +190,15 @@ let _unknownEmitted = false; // across files in one run — the def file imports
190
190
  // built only from decidable atoms (comparisons, Bool-returning calls) coerces fine
191
191
  // and stays in the more proof-friendly Prop form.
192
192
  let _boolCtx = false;
193
+ /** Constructor names come from source strings (string-union values,
194
+ * discriminated-union tags) and may contain non-identifier characters
195
+ * ("spec-pure"); guillemet-quote those — exact and collision-free. */
196
+ function leanCtorName(name) {
197
+ return /^[A-Za-z_][A-Za-z0-9_'!?]*$/.test(name) ? name : `«${name}»`;
198
+ }
193
199
  /** Render a match pattern to Lean syntax: `_`, `.none`, `.some x`, `.syn seq`. */
194
200
  function renderLeanPattern(p) {
195
- return p.kind === "wild" ? "_" : "." + [p.ctor, ...p.binders].join(" ");
201
+ return p.kind === "wild" ? "_" : "." + [leanCtorName(p.ctor), ...p.binders].join(" ");
196
202
  }
197
203
  // A Bool-valued atom that does NOT coerce to Prop: an inlined union discriminator
198
204
  // (lowered to a match-bool `match x with | .C .. => true | _ => false`) or a raw
@@ -313,6 +319,8 @@ function emitExpr(e, parentPrec) {
313
319
  // `undefined` is the IR's spelling of the absent optional (mirrors dafny-emit's None)
314
320
  case "var": return e.name === "undefined" ? "none" : escapeName(e.name);
315
321
  case "num": return `${e.value}`;
322
+ // Already canonical decimal; Lean's `Int` is mathematical, so no `n` suffix.
323
+ case "bigint": return e.value;
316
324
  case "bool": return e.value ? "true" : "false";
317
325
  case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
318
326
  case "constructor": {
@@ -320,7 +328,7 @@ function emitExpr(e, parentPrec) {
320
328
  // like `match ... | .none => Type.some x` where elaboration can't infer).
321
329
  // Without type: emit `.name` (dotted form; works in pattern positions
322
330
  // and where the expected type is clear from context).
323
- const head = e.type ? `${e.type}.${e.name}` : `.${e.name}`;
331
+ const head = e.type ? `${e.type}.${leanCtorName(e.name)}` : `.${leanCtorName(e.name)}`;
324
332
  if (!e.args || e.args.length === 0)
325
333
  return head;
326
334
  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));
@@ -361,9 +369,10 @@ function emitExpr(e, parentPrec) {
361
369
  case "unop":
362
370
  if (e.op === "¬")
363
371
  return _boolCtx ? `!(${emitExpr(e.expr)})` : `¬(${emitExpr(e.expr)})`;
364
- if (e.op === "-" && e.expr.kind === "num")
365
- return `-${e.expr.value}`;
366
- return `(-${emitExpr(e.expr)})`;
372
+ if (e.op !== "-")
373
+ throw new Error(`Unsupported Lean unary operator: ${e.op}`);
374
+ return (e.expr.kind === "num" || e.expr.kind === "bigint")
375
+ ? `-${e.expr.value}` : `(-${emitExpr(e.expr)})`;
367
376
  case "binop": {
368
377
  // Discriminator test against a constructor that carries fields:
369
378
  // `x = .Ctor` → `(match x with | .Ctor .. => true | _ => false)`. A
@@ -376,7 +385,7 @@ function emitExpr(e, parentPrec) {
376
385
  const ctor = rhs.type ? _unionCtors.get(rhs.type)?.find(c => c.name === rhs.name) : undefined;
377
386
  if (ctor && ctor.fields.length > 0) {
378
387
  const [yes, no] = e.op === "=" ? ["true", "false"] : ["false", "true"];
379
- return `(match ${emitExpr(e.left)} with | .${escapeName(rhs.name)} .. => ${yes} | _ => ${no})`;
388
+ return `(match ${emitExpr(e.left)} with | .${leanCtorName(rhs.name)} .. => ${yes} | _ => ${no})`;
380
389
  }
381
390
  }
382
391
  // `k in m` (map/set membership) → `m.contains k` in Lean. Dafny has
@@ -415,8 +424,10 @@ function emitExpr(e, parentPrec) {
415
424
  // Datatype constructor (tagged by transform): Lean needs the qualified name
416
425
  // `BaseType.variant`; a bare `variant` is an unknown identifier. (Dafny keeps
417
426
  // the bare form, so its output is unaffected.)
418
- if (e.ctorOf)
419
- return args.length ? `${e.ctorOf}.${e.fn} ${args.join(" ")}` : `${e.ctorOf}.${e.fn}`;
427
+ if (e.ctorOf) {
428
+ const ctor = leanCtorName(e.fn);
429
+ return args.length ? `${e.ctorOf}.${ctor} ${args.join(" ")}` : `${e.ctorOf}.${ctor}`;
430
+ }
420
431
  // Option constructors arrive Dafny-spelled from transform (`app "Some"`);
421
432
  // Lean core exports the lowercase forms as top-level names.
422
433
  if (e.fn === "Some" && args.length === 1)
@@ -460,18 +471,18 @@ function emitExpr(e, parentPrec) {
460
471
  const idx = owner.fields.findIndex(f => f.name === e.field);
461
472
  const pats = owner.fields.map((_, i) => (i === idx ? "_v" : "_")).join(" ");
462
473
  const fty = tyToLean(owner.fields[idx].type);
463
- return `(match ${emitExpr(e.obj)} with | .${escapeName(owner.name)} ${pats} => _v | _ => (default : ${fty}))`;
474
+ return `(match ${emitExpr(e.obj)} with | .${leanCtorName(owner.name)} ${pats} => _v | _ => (default : ${fty}))`;
464
475
  }
465
476
  }
466
477
  const obj = emitExpr(e.obj);
467
478
  if (e.field === "collectionSize")
468
479
  return `${obj}.size`;
469
- const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bool";
480
+ const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bigint" && e.obj.kind !== "bool";
470
481
  return wrap ? `(${obj}).${escapeName(e.field)}` : `${obj}.${escapeName(e.field)}`;
471
482
  }
472
483
  case "toNat": {
473
484
  const inner = emitExpr(e.expr);
474
- const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
485
+ const wrap = e.expr.kind !== "var" && e.expr.kind !== "num" && e.expr.kind !== "bigint";
475
486
  return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
476
487
  }
477
488
  case "toReal":
@@ -498,7 +509,7 @@ function emitExpr(e, parentPrec) {
498
509
  // so any token after an arm body (`→`, another match's `|`, etc.) would
499
510
  // bleed into the last `.none` case without explicit bracketing.
500
511
  const arms = e.arms.map(a => `| ${renderLeanPattern(a.pattern)} => ${emitExpr(a.body)}`);
501
- const scrut = typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee);
512
+ const scrut = emitExpr(e.scrutinee);
502
513
  return `(match ${scrut} with ${arms.join(" ")})`;
503
514
  }
504
515
  case "forall": return `∀ ${e.var} : ${tyToLean(e.type)}, ${emitExpr(e.body)}`;
@@ -586,7 +597,7 @@ function emitStmt(s, indent) {
586
597
  return out;
587
598
  }
588
599
  case "match": {
589
- const scrut = typeof s.scrutinee === "string" ? s.scrutinee : emitExpr(s.scrutinee);
600
+ const scrut = emitExpr(s.scrutinee);
590
601
  // Option match (.some/.none) → emit as if/let for WPGen.if compatibility
591
602
  if (s.arms.length === 2) {
592
603
  const someArm = s.arms.find(a => a.pattern.kind === "ctor" && a.pattern.ctor === "some");
@@ -718,11 +729,11 @@ function emitDecl(d) {
718
729
  const lines = [`inductive ${d.name} where`];
719
730
  for (const c of d.constructors) {
720
731
  if (c.fields.length === 0) {
721
- lines.push(` | ${c.name} : ${d.name}`);
732
+ lines.push(` | ${leanCtorName(c.name)} : ${d.name}`);
722
733
  }
723
734
  else {
724
735
  const params = c.fields.map(f => `(${escapeName(f.name)} : ${tyToLean(f.type)})`).join(" ");
725
- lines.push(` | ${c.name} ${params} : ${d.name}`);
736
+ lines.push(` | ${leanCtorName(c.name)} ${params} : ${d.name}`);
726
737
  }
727
738
  }
728
739
  return lines.join("\n") + emitDeriving(d.name, d.deriving);
@@ -775,7 +786,7 @@ function emitDecl(d) {
775
786
  // Prime the return binder only on a collision within *this method's own*
776
787
  // signature/body — `res` is a common identifier module-wide (record
777
788
  // fields, unrelated params), so a module-wide check would prime spuriously.
778
- _resultName = freshName("res", n => d.params.some(p => escapeName(p.name) === n) ||
789
+ _resultName = freshNameWhere("res", n => d.params.some(p => escapeName(p.name) === n) ||
779
790
  usesNameInDecl(d.requires, d.ensures, d.body, n));
780
791
  const lines = [`method ${d.name} ${params} return (${_resultName} : ${tyToLean(d.returnType)})`];
781
792
  for (const r of d.requires)
@@ -829,7 +840,7 @@ function emitPureExpr(e, indent) {
829
840
  case "if":
830
841
  return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
831
842
  case "match": {
832
- const lines = [`${pad}match ${typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee)} with`];
843
+ const lines = [`${pad}match ${emitExpr(e.scrutinee)} with`];
833
844
  for (const arm of e.arms) {
834
845
  lines.push(`${pad}| ${renderLeanPattern(arm.pattern)} =>`);
835
846
  lines.push(emitPureExpr(arm.body, indent + 1));
package/tools/dist/lsc.js CHANGED
@@ -16,12 +16,25 @@ import { autoHavocModule } from "./autohavoc.js";
16
16
  import { transformModuleLean, transformModuleDafny } from "./transform.js";
17
17
  import { peepholeModule } from "./peephole.js";
18
18
  import { emitLeanFile, resetLeanModule } from "./lean-emit.js";
19
- import { emitDafnyFile } from "./dafny-emit.js";
19
+ import { emitDafnyFile, emittedNameMap } from "./dafny-emit.js";
20
20
  import { dafnyGen, dafnyCheckDiff, dafnyVerify, dafnyRegen } from "./dafny-commands.js";
21
21
  import { leanGen, leanCheck } from "./lean-commands.js";
22
- import { runInfo } from "./info-command.js";
22
+ import { runInfo, runTypedInfo } from "./info-command.js";
23
+ /** Version of the lemmascript package — the root package.json sits two levels
24
+ * above this module from both tools/src/ (tsx) and tools/dist/ (installed). */
25
+ function lscVersion() {
26
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
27
+ return pkg.version;
28
+ }
23
29
  function main() {
24
30
  const args = process.argv.slice(2);
31
+ // `lsc version` — print the package version. Machine consumers (satellites
32
+ // like lemmascript-claimcheck/-crosscheck) use this for their version
33
+ // handshake; keep the output to the bare semver string.
34
+ if (args[0] === "version") {
35
+ console.log(lscVersion());
36
+ return;
37
+ }
25
38
  // `lsc claimcheck <file.ts> …` forwards verbatim to the lemmascript-claimcheck
26
39
  // CLI (a dependency; its cli reads the rewritten process.argv). With no
27
40
  // leading <file.ts>, batch: one satellite run per LemmaScript-files.txt entry,
@@ -108,6 +121,14 @@ function main() {
108
121
  noVerify = true;
109
122
  args.splice(noVerifyIdx, 1);
110
123
  }
124
+ // --typed (info only): print the machine-readable Typed IR contract to
125
+ // stdout instead of writing the human-oriented foo.ts.json.
126
+ let typedInfo = false;
127
+ const typedIdx = args.indexOf("--typed");
128
+ if (typedIdx >= 0) {
129
+ typedInfo = true;
130
+ args.splice(typedIdx, 1);
131
+ }
111
132
  // Anything flag-shaped left over is a typo or a space-separated form
112
133
  // (`--backend lean`): reject it rather than let it become a positional arg
113
134
  // or be silently ignored (which would e.g. verify with the wrong backend).
@@ -119,15 +140,21 @@ function main() {
119
140
  const [cmd, filePath] = args;
120
141
  if (!cmd) {
121
142
  console.error("Usage: lsc <gen|check|regen|extract|info> [--backend=lean|dafny] <file.ts>");
143
+ console.error(" lsc info --typed <file.ts> (machine-readable Typed IR contract to stdout)");
122
144
  console.error(" lsc <gen|gen-check|check> [--backend=…] [--slow] (no file: batch over LemmaScript-files.txt)");
123
145
  console.error(" lsc claimcheck [<file.ts>] [flags…] (forwards to lemmascript-claimcheck)");
146
+ console.error(" lsc version");
147
+ process.exit(1);
148
+ }
149
+ if (typedInfo && cmd !== "info") {
150
+ console.error(`--typed is only valid with the info command (got: ${cmd})`);
124
151
  process.exit(1);
125
152
  }
126
153
  if (!filePath) {
127
154
  runBatch(cmd, backend, slow);
128
155
  return;
129
156
  }
130
- runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify);
157
+ runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify, typedInfo);
131
158
  }
132
159
  // LemmaScript-files.txt, parsed: `filepath [timeout_in_seconds] [extra dafny
133
160
  // flags…]` per line; no timeout = Dafny default. Exits if the file is absent.
@@ -164,7 +191,7 @@ function runBatch(cmd, backend, slow) {
164
191
  }
165
192
  }
166
193
  }
167
- function runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify = false) {
194
+ function runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify = false, typedInfo = false) {
168
195
  const absPath = path.resolve(filePath);
169
196
  if (!existsSync(absPath)) {
170
197
  console.error(`File not found: ${absPath}`);
@@ -212,7 +239,7 @@ function runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify = false
212
239
  console.log(JSON.stringify(raw, null, 2));
213
240
  return;
214
241
  }
215
- if (cmd === "info") {
242
+ if (cmd === "info" && !typedInfo) {
216
243
  const outPath = path.join(path.dirname(absPath), `${path.basename(filePath, ".ts")}.ts.json`);
217
244
  runInfo(raw, outPath);
218
245
  return;
@@ -224,6 +251,27 @@ function runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify = false
224
251
  // values so verification rests only on the declared contracts (a sound
225
252
  // over-approximation). No-op unless a function opts in.
226
253
  const typed = autoHavocModule(narrowModule(resolved));
254
+ if (cmd === "info") {
255
+ // --typed: the satellite contract. Run an in-memory Dafny emission purely
256
+ // to harvest the emitted-name map; the file is backend-neutral otherwise,
257
+ // so a failure here (e.g. Lean-only constructs) degrades to an error note
258
+ // rather than failing the command.
259
+ let dafnyInfo;
260
+ try {
261
+ let { typesFile, defFile } = transformModuleDafny(typed);
262
+ if (typesFile)
263
+ typesFile = peepholeModule(typesFile, "dafny");
264
+ defFile = peepholeModule(defFile, "dafny");
265
+ const merged = { ...defFile, decls: [...(typesFile?.decls ?? []), ...defFile.decls] };
266
+ emitDafnyFile(merged, path.basename(filePath), { safeSlice });
267
+ dafnyInfo = { emittedNames: Object.fromEntries(emittedNameMap()) };
268
+ }
269
+ catch (err) {
270
+ dafnyInfo = { error: err instanceof Error ? err.message : String(err) };
271
+ }
272
+ runTypedInfo(raw, typed, lscVersion(), backendDirective ? backendDirective[1] : null, dafnyInfo);
273
+ return;
274
+ }
227
275
  const dir = path.dirname(absPath);
228
276
  const base = path.basename(filePath, ".ts");
229
277
  // ── Dafny backend ─────────────────────────────────────────
@@ -39,12 +39,16 @@ export function isUserName(name) {
39
39
  export function userNames() {
40
40
  return [..._userNames];
41
41
  }
42
- /** A toolchain-internal name: `base` verbatim, primed on collision. The one
43
- * place the priming rule lives. `taken` says what counts as a collision —
44
- * by default a user-written name anywhere in the module; callers that know
45
- * the exact scope (e.g. a comprehension binder checking only the expressions
46
- * it wraps) pass their own predicate. */
47
- export function freshName(base, taken = isUserName) {
42
+ /** A toolchain-internal name: `base` verbatim, primed on collision against
43
+ * user-written names anywhere in the module. The common form deterministic
44
+ * per module. */
45
+ export function freshName(base) {
46
+ return freshNameWhere(base, isUserName);
47
+ }
48
+ /** The priming rule, against a caller-chosen collision predicate — for
49
+ * callers that know the exact scope (e.g. a comprehension binder checking
50
+ * only the expressions it wraps). The one place the rule lives. */
51
+ export function freshNameWhere(base, taken) {
48
52
  let name = base;
49
53
  while (taken(name))
50
54
  name += "'";