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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/tools/dist/autohavoc.js +2 -0
- package/tools/dist/builtins.js +125 -0
- package/tools/dist/condition-facts.js +364 -0
- package/tools/dist/dafny-emit.js +228 -37
- package/tools/dist/extract.js +175 -37
- package/tools/dist/info-command.js +68 -0
- package/tools/dist/ir.js +27 -7
- package/tools/dist/lean-emit.js +29 -18
- package/tools/dist/lsc.js +53 -5
- package/tools/dist/names.js +10 -6
- package/tools/dist/narrow.js +296 -677
- package/tools/dist/peephole.js +12 -94
- package/tools/dist/rawir.js +15 -1
- package/tools/dist/resolve.js +268 -249
- package/tools/dist/specparser.js +21 -17
- package/tools/dist/transform.js +411 -131
- package/tools/dist/typedecls.js +59 -0
package/tools/dist/lean-emit.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and support-import selection.
|
|
6
6
|
*/
|
|
7
7
|
import { anyExpr, usesNameInDecl, patternBinders } from "./ir.js";
|
|
8
|
-
import {
|
|
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
|
|
365
|
-
|
|
366
|
-
return
|
|
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 | .${
|
|
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
|
-
|
|
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 | .${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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 ─────────────────────────────────────────
|
package/tools/dist/names.js
CHANGED
|
@@ -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
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 += "'";
|