lemmascript 0.5.7 → 0.5.9
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/dafny-emit.js +50 -4
- package/tools/dist/extract.js +106 -39
- package/tools/dist/ir.js +56 -1
- package/tools/dist/lean-emit.js +242 -22
- package/tools/dist/lsc.js +14 -6
- package/tools/dist/narrow.js +89 -2
- package/tools/dist/peephole.js +2 -0
- package/tools/dist/resolve.js +147 -5
- package/tools/dist/transform.js +227 -9
- package/tools/dist/types.js +14 -0
- package/tools/dist/guard-command.js +0 -238
package/tools/dist/lean-emit.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Lean emitter — IR → Lean text.
|
|
3
3
|
* No logic, no type decisions — just serialization.
|
|
4
4
|
*/
|
|
5
|
+
import { anyExpr } from "./ir.js";
|
|
5
6
|
// ── Ty → Lean type string ──────────────────────────────────
|
|
6
7
|
function tyToLean(ty) {
|
|
7
8
|
switch (ty.kind) {
|
|
@@ -55,7 +56,14 @@ function tyToLean(ty) {
|
|
|
55
56
|
const retStr = ret.includes(" ") ? `(${ret})` : ret;
|
|
56
57
|
return [...params, retStr].join(" → ");
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
+
// Out-of-subset (`any`/`unknown`) — an opaque carrier, so unmodeled payloads
|
|
60
|
+
// (e.g. a `details` field never inspected by the verified code) pass through
|
|
61
|
+
// but real ops on them fail loudly. Mirrors the Dafny backend's
|
|
62
|
+
// `type Unknown(==, 0)`. The decl is emitted once, in the first file that
|
|
63
|
+
// needs it (the def file imports the types file, so no duplicate).
|
|
64
|
+
case "unknown":
|
|
65
|
+
_needsUnknown = true;
|
|
66
|
+
return "Unknown";
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
// ── Lean keyword escaping ────────────────────────────────────
|
|
@@ -83,8 +91,119 @@ const PREC = {
|
|
|
83
91
|
"+": 5, "-": 5, "++": 5, "arrayConcat": 5, "*": 6, "/": 6, "%": 6,
|
|
84
92
|
};
|
|
85
93
|
function prec(op) { return PREC[op] ?? 10; }
|
|
86
|
-
|
|
94
|
+
const _unionCtors = new Map();
|
|
95
|
+
// Types that transitively reference an `opaque` type can't derive `Repr` or
|
|
96
|
+
// `DecidableEq` (the opaque type provides neither). `Inhabited` still derives
|
|
97
|
+
// (via the empty array / first constructor), so only those two are dropped.
|
|
98
|
+
// "Unknown" (the `any`/`unknown` carrier) is opaque by construction.
|
|
99
|
+
const _opaqueNames = new Set(["Unknown"]);
|
|
100
|
+
const _typeRefs = new Map();
|
|
101
|
+
const _taintedTypes = new Set();
|
|
102
|
+
function collectUserRefs(ty, into) {
|
|
103
|
+
switch (ty.kind) {
|
|
104
|
+
case "array":
|
|
105
|
+
case "set":
|
|
106
|
+
collectUserRefs(ty.elem, into);
|
|
107
|
+
break;
|
|
108
|
+
case "optional":
|
|
109
|
+
collectUserRefs(ty.inner, into);
|
|
110
|
+
break;
|
|
111
|
+
case "map":
|
|
112
|
+
collectUserRefs(ty.key, into);
|
|
113
|
+
collectUserRefs(ty.value, into);
|
|
114
|
+
break;
|
|
115
|
+
case "fn":
|
|
116
|
+
ty.params.forEach(p => collectUserRefs(p, into));
|
|
117
|
+
collectUserRefs(ty.result, into);
|
|
118
|
+
break;
|
|
119
|
+
case "user":
|
|
120
|
+
into.add(ty.name.includes("<") ? ty.name.slice(0, ty.name.indexOf("<")) : ty.name);
|
|
121
|
+
break;
|
|
122
|
+
case "unknown":
|
|
123
|
+
into.add("Unknown");
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function registerInductives(decls) {
|
|
128
|
+
for (const d of decls) {
|
|
129
|
+
if (d.kind === "inductive") {
|
|
130
|
+
_unionCtors.set(d.name, d.constructors);
|
|
131
|
+
const refs = new Set();
|
|
132
|
+
for (const c of d.constructors)
|
|
133
|
+
for (const f of c.fields)
|
|
134
|
+
collectUserRefs(f.type, refs);
|
|
135
|
+
_typeRefs.set(d.name, refs);
|
|
136
|
+
}
|
|
137
|
+
else if (d.kind === "structure") {
|
|
138
|
+
const refs = new Set();
|
|
139
|
+
for (const f of d.fields)
|
|
140
|
+
collectUserRefs(f.type, refs);
|
|
141
|
+
_typeRefs.set(d.name, refs);
|
|
142
|
+
}
|
|
143
|
+
else if (d.kind === "opaque-type") {
|
|
144
|
+
_opaqueNames.add(d.name);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Fixpoint: a type is tainted if it references an opaque or already-tainted type.
|
|
148
|
+
let changed = true;
|
|
149
|
+
while (changed) {
|
|
150
|
+
changed = false;
|
|
151
|
+
for (const [name, refs] of _typeRefs) {
|
|
152
|
+
if (_taintedTypes.has(name))
|
|
153
|
+
continue;
|
|
154
|
+
if ([...refs].some(r => _opaqueNames.has(r) || _taintedTypes.has(r))) {
|
|
155
|
+
_taintedTypes.add(name);
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Deriving clause, dropping `Repr`/`DecidableEq` for opaque-tainted types. */
|
|
162
|
+
function emitDeriving(name, deriving) {
|
|
163
|
+
const der = _taintedTypes.has(name) ? deriving.filter(x => x !== "Repr" && x !== "DecidableEq") : deriving;
|
|
164
|
+
return der.length > 0 ? `\nderiving ${der.join(", ")}` : "";
|
|
165
|
+
}
|
|
87
166
|
let _needsJSString = false;
|
|
167
|
+
let _needsUnknown = false;
|
|
168
|
+
let _unknownEmitted = false; // across files in one run — the def file imports the types file
|
|
169
|
+
// Bool-vs-Prop context. Lean keeps `Bool` and `Prop` distinct; the IR uses the
|
|
170
|
+
// Prop connectives (∧/∨/¬) uniformly. In a computational position the connectives
|
|
171
|
+
// may need to be the Bool ones (&&/||/!). They are only *required* when a connective
|
|
172
|
+
// has an operand that does not coerce Bool→Prop — see `needsBoolConnectives`. A body
|
|
173
|
+
// built only from decidable atoms (comparisons, Bool-returning calls) coerces fine
|
|
174
|
+
// and stays in the more proof-friendly Prop form.
|
|
175
|
+
let _boolCtx = false;
|
|
176
|
+
// A Bool-valued atom that does NOT coerce to Prop: an inlined union discriminator
|
|
177
|
+
// (lowered to a match-bool `match x with | .C .. => true | _ => false`) or a raw
|
|
178
|
+
// `match` used as a Bool — neither has a `Decidable` instance Lean can synthesize
|
|
179
|
+
// through the `match`. Under a Prop connective (∧/∨/¬) such an operand is a type
|
|
180
|
+
// error, so its presence forces the whole body to Bool connectives.
|
|
181
|
+
function isNonCoercibleBoolAtom(e) {
|
|
182
|
+
if (e.kind === "match")
|
|
183
|
+
return true;
|
|
184
|
+
if (e.kind === "binop" && (e.op === "=" || e.op === "≠") && e.right.kind === "constructor") {
|
|
185
|
+
const rhs = e.right; // capture the narrowed node so it survives the closure below
|
|
186
|
+
const ctor = rhs.type ? _unionCtors.get(rhs.type)?.find(c => c.name === rhs.name) : undefined;
|
|
187
|
+
return !!ctor && ctor.fields.length > 0;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
// A ∧/∨/¬ connective with a non-coercible operand — the specific node that would
|
|
192
|
+
// fail to elaborate if emitted in Prop form.
|
|
193
|
+
function connectiveHasNonCoercibleOperand(e) {
|
|
194
|
+
if (e.kind === "binop" && (e.op === "∧" || e.op === "∨"))
|
|
195
|
+
return isNonCoercibleBoolAtom(e.left) || isNonCoercibleBoolAtom(e.right);
|
|
196
|
+
if (e.kind === "unop" && e.op === "¬")
|
|
197
|
+
return isNonCoercibleBoolAtom(e.expr);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
// True iff some ∧/∨/¬ connective in `e` has a non-coercible operand, i.e. emitting
|
|
201
|
+
// the body with Prop connectives would fail to elaborate. (A decidable atom coerces,
|
|
202
|
+
// so a body with no such operand stays Prop — this keeps arithmetic predicates like
|
|
203
|
+
// `x ≥ 0 ∧ x < n` in `∧` form rather than forcing `&&`.)
|
|
204
|
+
function needsBoolConnectives(e) {
|
|
205
|
+
return anyExpr(e, connectiveHasNonCoercibleOperand);
|
|
206
|
+
}
|
|
88
207
|
function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
89
208
|
// Array methods
|
|
90
209
|
if (tyKind === "array") {
|
|
@@ -100,10 +219,14 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
100
219
|
return args.length > 1 ? `(${obj}.extract ${args[1]} ${obj}.size).contains ${args[0]}` : `${obj}.contains ${args[0]}`;
|
|
101
220
|
if (method === "find")
|
|
102
221
|
return `${obj}.find? ${args[0]}`;
|
|
222
|
+
if (method === "join")
|
|
223
|
+
return `(String.intercalate ${args[0]} ${obj}.toList)`;
|
|
103
224
|
if (method === "with")
|
|
104
225
|
return `${obj}.set! ${args[0]} ${args[1]}`;
|
|
105
226
|
if (method === "push")
|
|
106
227
|
return args.length === 1 ? `Array.push ${obj} ${args[0]}` : `${obj} ++ #[${args.join(", ")}]`;
|
|
228
|
+
if (method === "unshift")
|
|
229
|
+
return `(#[${args.join(", ")}] ++ ${obj})`;
|
|
107
230
|
if (method === "concat")
|
|
108
231
|
return args.length === 1 ? `Array.push ${obj} ${args[0]}` : `${obj} ++ #[${args.join(", ")}]`;
|
|
109
232
|
// arr.slice → Array.extract. No-arg slice is a full copy (Array is a value
|
|
@@ -162,7 +285,8 @@ function wrapOperand(sub, parentPrec) {
|
|
|
162
285
|
}
|
|
163
286
|
function emitExpr(e, parentPrec) {
|
|
164
287
|
switch (e.kind) {
|
|
165
|
-
|
|
288
|
+
// `undefined` is the IR's spelling of the absent optional (mirrors dafny-emit's None)
|
|
289
|
+
case "var": return e.name === "undefined" ? "none" : escapeName(e.name);
|
|
166
290
|
case "num": return `${e.value}`;
|
|
167
291
|
case "bool": return e.value ? "true" : "false";
|
|
168
292
|
case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
@@ -183,6 +307,7 @@ function emitExpr(e, parentPrec) {
|
|
|
183
307
|
return `#[${e.elems.map(el => emitExpr(el)).join(", ")}]`;
|
|
184
308
|
case "emptyMap": return `Std.HashMap.empty`;
|
|
185
309
|
case "emptySet": return `Std.HashSet.empty`;
|
|
310
|
+
case "default": return `(default : ${tyToLean(e.type)})`;
|
|
186
311
|
case "methodCall": {
|
|
187
312
|
const obj = emitExpr(e.obj);
|
|
188
313
|
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "methodCall" || e.obj.kind === "if" || e.obj.kind === "let";
|
|
@@ -201,11 +326,25 @@ function emitExpr(e, parentPrec) {
|
|
|
201
326
|
}
|
|
202
327
|
case "unop":
|
|
203
328
|
if (e.op === "¬")
|
|
204
|
-
return `¬(${emitExpr(e.expr)})`;
|
|
329
|
+
return _boolCtx ? `!(${emitExpr(e.expr)})` : `¬(${emitExpr(e.expr)})`;
|
|
205
330
|
if (e.op === "-" && e.expr.kind === "num")
|
|
206
331
|
return `-${e.expr.value}`;
|
|
207
332
|
return `(-${emitExpr(e.expr)})`;
|
|
208
333
|
case "binop": {
|
|
334
|
+
// Discriminator test against a constructor that carries fields:
|
|
335
|
+
// `x = .Ctor` → `(match x with | .Ctor .. => true | _ => false)`. A
|
|
336
|
+
// multi-field constructor is a function, not a value, so a bare
|
|
337
|
+
// `x = Type.Ctor` is ill-typed. The Bool result coerces to Prop in spec
|
|
338
|
+
// positions, mirroring how the backend already treats decidable atoms.
|
|
339
|
+
// Nullary constructors (enums) keep the cheap `DecidableEq` comparison.
|
|
340
|
+
if ((e.op === "=" || e.op === "≠") && e.right.kind === "constructor") {
|
|
341
|
+
const rhs = e.right; // capture the narrowed node so it survives into the closure below
|
|
342
|
+
const ctor = rhs.type ? _unionCtors.get(rhs.type)?.find(c => c.name === rhs.name) : undefined;
|
|
343
|
+
if (ctor && ctor.fields.length > 0) {
|
|
344
|
+
const [yes, no] = e.op === "=" ? ["true", "false"] : ["false", "true"];
|
|
345
|
+
return `(match ${emitExpr(e.left)} with | .${escapeName(rhs.name)} .. => ${yes} | _ => ${no})`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
209
348
|
// `k in m` (map/set membership) → `m.contains k` in Lean. Dafny has
|
|
210
349
|
// native `in`; Lean uses the method form for HashMap/HashSet.
|
|
211
350
|
if (e.op === "in") {
|
|
@@ -213,7 +352,10 @@ function emitExpr(e, parentPrec) {
|
|
|
213
352
|
const wrap = e.right.kind === "binop" || e.right.kind === "app" || e.right.kind === "methodCall";
|
|
214
353
|
return `${wrap ? `(${recv})` : recv}.contains ${emitExpr(e.left)}`;
|
|
215
354
|
}
|
|
216
|
-
const op = e.op === "arrayConcat" ? "++"
|
|
355
|
+
const op = e.op === "arrayConcat" ? "++"
|
|
356
|
+
: _boolCtx && e.op === "∧" ? "&&"
|
|
357
|
+
: _boolCtx && e.op === "∨" ? "||"
|
|
358
|
+
: e.op;
|
|
217
359
|
// ↔ does not chain in Lean — a nested iff operand needs parens.
|
|
218
360
|
const childPrec = e.op === "↔" ? prec(e.op) + 1 : prec(e.op);
|
|
219
361
|
// `-`, `/`, `%` are left-associative and non-associative, so an equal-
|
|
@@ -241,6 +383,12 @@ function emitExpr(e, parentPrec) {
|
|
|
241
383
|
// the bare form, so its output is unaffected.)
|
|
242
384
|
if (e.ctorOf)
|
|
243
385
|
return args.length ? `${e.ctorOf}.${e.fn} ${args.join(" ")}` : `${e.ctorOf}.${e.fn}`;
|
|
386
|
+
// Option constructors arrive Dafny-spelled from transform (`app "Some"`);
|
|
387
|
+
// Lean core exports the lowercase forms as top-level names.
|
|
388
|
+
if (e.fn === "Some" && args.length === 1)
|
|
389
|
+
return `some ${args[0]}`;
|
|
390
|
+
if (e.fn === "None" && args.length === 0)
|
|
391
|
+
return `none`;
|
|
244
392
|
// SetToSeq → .toArray for Lean (HashSet has native toArray)
|
|
245
393
|
if (e.fn === "SetToSeq" && args.length === 1)
|
|
246
394
|
return `${args[0]}.toArray`;
|
|
@@ -263,6 +411,24 @@ function emitExpr(e, parentPrec) {
|
|
|
263
411
|
return `${e.fn} ${args.join(" ")}`;
|
|
264
412
|
}
|
|
265
413
|
case "field": {
|
|
414
|
+
// Union destructor (tagged by transform): `x.field` where x is a
|
|
415
|
+
// multi-constructor inductive. Lean has no projection there, so match the
|
|
416
|
+
// owning constructor, bind the field positionally, and ignore the rest.
|
|
417
|
+
// Other constructors fall to `default` — the source guards every such
|
|
418
|
+
// access with a discriminator test, so that branch is never reached.
|
|
419
|
+
if (e.fromUnion) {
|
|
420
|
+
const ctors = _unionCtors.get(e.fromUnion);
|
|
421
|
+
// Pin the owning ctor when given (field names repeat across variants);
|
|
422
|
+
// otherwise fall back to the sole variant carrying this field name.
|
|
423
|
+
const owner = (e.ctor ? ctors?.find(c => c.name === e.ctor) : undefined)
|
|
424
|
+
?? ctors?.find(c => c.fields.some(f => f.name === e.field));
|
|
425
|
+
if (owner) {
|
|
426
|
+
const idx = owner.fields.findIndex(f => f.name === e.field);
|
|
427
|
+
const pats = owner.fields.map((_, i) => (i === idx ? "_v" : "_")).join(" ");
|
|
428
|
+
const fty = tyToLean(owner.fields[idx].type);
|
|
429
|
+
return `(match ${emitExpr(e.obj)} with | .${escapeName(owner.name)} ${pats} => _v | _ => (default : ${fty}))`;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
266
432
|
const obj = emitExpr(e.obj);
|
|
267
433
|
if (e.field === "collectionSize")
|
|
268
434
|
return `${obj}.size`;
|
|
@@ -328,10 +494,19 @@ function emitStmts(stmts, indent) {
|
|
|
328
494
|
function emitStmt(s, indent) {
|
|
329
495
|
const pad = " ".repeat(indent);
|
|
330
496
|
switch (s.kind) {
|
|
331
|
-
case "let":
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
497
|
+
case "let": {
|
|
498
|
+
// `mut` lets always carry their type (assignments must re-elaborate at it).
|
|
499
|
+
// Immutable lets are ascribed only when the initializer is a `match` (the
|
|
500
|
+
// `??` / Option-unwrap lowerings): Velvet's WP elaboration runs *backwards*,
|
|
501
|
+
// so without the ascription the binding's type is a metavariable that a
|
|
502
|
+
// later use pins first — e.g. `lines.size ≤ maxLines` pins `maxLines : ℕ`
|
|
503
|
+
// and the ℤ-valued unwrap arms then fail to elaborate. Other initializer
|
|
504
|
+
// shapes determine their own type, and leaving them bare keeps previously
|
|
505
|
+
// generated (and proven) artifacts byte-stable.
|
|
506
|
+
const ascribe = s.mutable || s.value.kind === "match";
|
|
507
|
+
const ty = ascribe && s.type.kind !== "unknown" ? ` : ${tyToLean(s.type)}` : "";
|
|
508
|
+
return `${pad}let ${s.mutable ? "mut " : ""}${escapeName(s.name)}${ty} := ${emitExpr(s.value)}`;
|
|
509
|
+
}
|
|
335
510
|
case "assign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
|
|
336
511
|
case "ghostLet":
|
|
337
512
|
return `${pad}let mut ${escapeName(s.name)} : ${tyToLean(s.type)} := ${emitExpr(s.value)}`;
|
|
@@ -344,7 +519,10 @@ function emitStmt(s, indent) {
|
|
|
344
519
|
// to WPGen.default, which drops the assertion. Coerce to Prop via `= true`.
|
|
345
520
|
// Top-level Prop constructs (`=`, `<`, `∧`, `¬`, `∀`, `∃`, `→`) already
|
|
346
521
|
// land in Prop — Lean auto-coerces inner Bools there.
|
|
522
|
+
const prevBoolCtx = _boolCtx;
|
|
523
|
+
_boolCtx = false; // assertions are Prop
|
|
347
524
|
const inner = emitExpr(s.expr);
|
|
525
|
+
_boolCtx = prevBoolCtx;
|
|
348
526
|
const wrapped = isPropValued(s.expr) ? inner : `(${inner}) = true`;
|
|
349
527
|
return `${pad}assertGadget (${wrapped})`;
|
|
350
528
|
}
|
|
@@ -411,21 +589,30 @@ function emitStmt(s, indent) {
|
|
|
411
589
|
return lines.join("\n");
|
|
412
590
|
}
|
|
413
591
|
case "while": {
|
|
592
|
+
// Guard is computational (Bool); invariants / done_with are Prop.
|
|
414
593
|
const lines = [`${pad}while ${emitExpr(s.cond)}`];
|
|
594
|
+
const prevBoolCtx = _boolCtx;
|
|
595
|
+
_boolCtx = false;
|
|
415
596
|
for (const inv of s.invariants)
|
|
416
597
|
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
598
|
+
// `done_with True` (the auto-supplied fact for breaking loops) is a Prop;
|
|
599
|
+
// the bool literal would need a coercion, so emit the Prop `True` directly.
|
|
417
600
|
if (s.doneWith)
|
|
418
|
-
lines.push(`${pad} done_with ${emitExpr(s.doneWith)}`);
|
|
601
|
+
lines.push(`${pad} done_with ${s.doneWith.kind === "bool" && s.doneWith.value ? "True" : emitExpr(s.doneWith)}`);
|
|
419
602
|
if (s.decreasing)
|
|
420
603
|
lines.push(`${pad} decreasing ${emitExpr(s.decreasing)}`);
|
|
604
|
+
_boolCtx = prevBoolCtx;
|
|
421
605
|
lines.push(`${pad}do`);
|
|
422
606
|
lines.push(emitStmts(s.body, indent + 1));
|
|
423
607
|
return lines.join("\n");
|
|
424
608
|
}
|
|
425
609
|
case "forin": {
|
|
426
610
|
const lines = [`${pad}for ${s.idx} in [:${emitExpr(s.bound)}]`];
|
|
611
|
+
const prevBoolCtx = _boolCtx;
|
|
612
|
+
_boolCtx = false; // invariants are Prop
|
|
427
613
|
for (const inv of s.invariants)
|
|
428
614
|
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
615
|
+
_boolCtx = prevBoolCtx;
|
|
429
616
|
lines.push(`${pad}do`);
|
|
430
617
|
lines.push(emitStmts(s.body, indent + 1));
|
|
431
618
|
return lines.join("\n");
|
|
@@ -446,17 +633,13 @@ function emitDecl(d) {
|
|
|
446
633
|
lines.push(` | ${c.name} ${params} : ${d.name}`);
|
|
447
634
|
}
|
|
448
635
|
}
|
|
449
|
-
|
|
450
|
-
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
451
|
-
return lines.join("\n");
|
|
636
|
+
return lines.join("\n") + emitDeriving(d.name, d.deriving);
|
|
452
637
|
}
|
|
453
638
|
case "structure": {
|
|
454
639
|
const lines = [`structure ${d.name} where`];
|
|
455
640
|
for (const f of d.fields)
|
|
456
641
|
lines.push(` ${escapeName(f.name)} : ${tyToLean(f.type)}`);
|
|
457
|
-
|
|
458
|
-
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
459
|
-
return lines.join("\n");
|
|
642
|
+
return lines.join("\n") + emitDeriving(d.name, d.deriving);
|
|
460
643
|
}
|
|
461
644
|
case "type-alias": {
|
|
462
645
|
return `abbrev ${d.name} := ${tyToLean(d.target)}`;
|
|
@@ -467,7 +650,17 @@ function emitDecl(d) {
|
|
|
467
650
|
}
|
|
468
651
|
case "def": {
|
|
469
652
|
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
470
|
-
|
|
653
|
+
// A Bool-returning pure function is a computation, not a proposition, so its
|
|
654
|
+
// connectives *may* need the Bool operators (&&/||/!) — but only when a
|
|
655
|
+
// connective has a non-coercible operand (e.g. an inlined union discriminator).
|
|
656
|
+
// A predicate built from decidable atoms (`x ≥ 0 ∧ x < n`) coerces to Bool as
|
|
657
|
+
// a whole and stays in the more proof-friendly Prop form. Other return types
|
|
658
|
+
// only have connectives inside (Decidable) conditions, where Prop is fine.
|
|
659
|
+
const prevBoolCtx = _boolCtx;
|
|
660
|
+
_boolCtx = tyToLean(d.returnType) === "Bool" && needsBoolConnectives(d.body);
|
|
661
|
+
const body = emitPureExpr(d.body, 1);
|
|
662
|
+
_boolCtx = prevBoolCtx;
|
|
663
|
+
let out = `def ${d.name} ${params} : ${tyToLean(d.returnType)} :=\n${body}`;
|
|
471
664
|
// A `//@ decreases` on a pure function marks it recursive and names its
|
|
472
665
|
// termination measure — emit it as Lean's `termination_by`. This is
|
|
473
666
|
// required when the recursion is on `arr.slice(...)` (→ `Array.extract`,
|
|
@@ -484,13 +677,18 @@ function emitDecl(d) {
|
|
|
484
677
|
throw new Error("function by method is not supported for Lean backend");
|
|
485
678
|
case "method": {
|
|
486
679
|
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
680
|
+
// Spec clauses are Prop; the `do` body is computational (Bool).
|
|
681
|
+
const prevBoolCtx = _boolCtx;
|
|
682
|
+
_boolCtx = false;
|
|
487
683
|
const lines = [`method ${d.name} ${params} return (res : ${tyToLean(d.returnType)})`];
|
|
488
684
|
for (const r of d.requires)
|
|
489
685
|
lines.push(` require ${emitExpr(r)}`);
|
|
490
686
|
for (const e of d.ensures)
|
|
491
687
|
lines.push(` ensures ${emitExpr(e)}`);
|
|
492
688
|
lines.push(" do");
|
|
689
|
+
_boolCtx = true;
|
|
493
690
|
lines.push(emitStmts(d.body, 2));
|
|
691
|
+
_boolCtx = prevBoolCtx;
|
|
494
692
|
return lines.join("\n");
|
|
495
693
|
}
|
|
496
694
|
case "namespace": {
|
|
@@ -504,10 +702,25 @@ function emitDecl(d) {
|
|
|
504
702
|
throw new Error(`Lean class support not yet implemented: ${d.name}`);
|
|
505
703
|
case "const":
|
|
506
704
|
return `def ${escapeName(d.name)} : ${tyToLean(d.type)} := ${emitExpr(d.value)}`;
|
|
507
|
-
case "extern":
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
|
|
705
|
+
case "extern": {
|
|
706
|
+
// Mirror Dafny's `function {:axiom}`: an uninterpreted total function.
|
|
707
|
+
// In Lean that is an `opaque` declaration (sound — it commits to no body,
|
|
708
|
+
// only to the type being inhabited). Any `requires`/`ensures` the source
|
|
709
|
+
// carried become a characterizing `axiom`; `\result` was already replaced
|
|
710
|
+
// by the call expression in the transform, so ensures reference `name args`.
|
|
711
|
+
const tp = d.typeParams.length > 0 ? ` {${d.typeParams.join(" ")} : Type}` : "";
|
|
712
|
+
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
713
|
+
const sig = `opaque ${escapeName(d.name)}${tp}${params ? ` ${params}` : ""} : ${tyToLean(d.returnType)}`;
|
|
714
|
+
if (d.requires.length === 0 && d.ensures.length === 0)
|
|
715
|
+
return sig;
|
|
716
|
+
// Spec axiom: ∀ params, req1 → … → (ens1 ∧ … ∧ ensN). Tagged `@[grind]` so
|
|
717
|
+
// the proof automation can use it, matching the ghost-function convention.
|
|
718
|
+
const hyps = d.requires.map(emitExpr);
|
|
719
|
+
const concl = d.ensures.map(emitExpr).join(" ∧ ");
|
|
720
|
+
const axBody = [...hyps, concl].join(" → ");
|
|
721
|
+
const axiom = `@[grind] axiom ${escapeName(d.name)}_spec${params ? ` ${params}` : ""} : ${axBody}`;
|
|
722
|
+
return `${sig}\n${axiom}`;
|
|
723
|
+
}
|
|
511
724
|
}
|
|
512
725
|
}
|
|
513
726
|
/** Emit a pure expression with indented if/match blocks. */
|
|
@@ -533,12 +746,19 @@ function emitPureExpr(e, indent) {
|
|
|
533
746
|
// ── File emission ────────────────────────────────────────────
|
|
534
747
|
export function emitLeanFile(file) {
|
|
535
748
|
_needsJSString = false;
|
|
536
|
-
|
|
749
|
+
_needsUnknown = false;
|
|
750
|
+
registerInductives(file.decls);
|
|
751
|
+
// Emit declarations first so _needsJSString / _needsUnknown are set
|
|
537
752
|
const declLines = [];
|
|
538
753
|
for (const decl of file.decls) {
|
|
539
754
|
declLines.push("");
|
|
540
755
|
declLines.push(emitDecl(decl));
|
|
541
756
|
}
|
|
757
|
+
// The `unknown` carrier must precede every use (Lean is definition-before-use).
|
|
758
|
+
if (_needsUnknown && !_unknownEmitted) {
|
|
759
|
+
declLines.unshift("", "/-- Opaque carrier for `unknown`-typed values (mirrors Dafny's `type Unknown(==, 0)`). -/", "opaque Unknown : Type");
|
|
760
|
+
_unknownEmitted = true;
|
|
761
|
+
}
|
|
542
762
|
const lines = [];
|
|
543
763
|
if (file.comment) {
|
|
544
764
|
lines.push("/-");
|
package/tools/dist/lsc.js
CHANGED
|
@@ -82,6 +82,13 @@ function main() {
|
|
|
82
82
|
}
|
|
83
83
|
// File-level directives consumed by the Dafny emitter.
|
|
84
84
|
const safeSlice = /\/\/@ safe-slice\b/.test(fullText);
|
|
85
|
+
// `//@ lean-module <name>` overrides the Lean module base (default: file
|
|
86
|
+
// basename). Lean module names are flat/global, so two identically-named
|
|
87
|
+
// `.ts` files (e.g. an in-place fork's duplicated `compaction.ts`) would emit
|
|
88
|
+
// colliding `foo.types`/`foo.def` modules; this gives one a distinct base so
|
|
89
|
+
// both can be separate Lean libraries. Lean-only — Dafny is unaffected.
|
|
90
|
+
const leanModuleDirective = fullText.match(/\/\/@ lean-module ([A-Za-z0-9_.\-]+)/);
|
|
91
|
+
const leanModuleOverride = leanModuleDirective ? leanModuleDirective[1] : undefined;
|
|
85
92
|
// Extract: ts-morph → Raw IR
|
|
86
93
|
const raw = extractModule(sourceFile);
|
|
87
94
|
if (cmd === "extract") {
|
|
@@ -140,15 +147,16 @@ function main() {
|
|
|
140
147
|
process.exit(1);
|
|
141
148
|
}
|
|
142
149
|
// ── Lean backend ──────────────────────────────────────────
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
150
|
+
const leanBase = leanModuleOverride ?? base;
|
|
151
|
+
const specPath = path.join(dir, `${leanBase}.spec.lean`);
|
|
152
|
+
const specImport = existsSync(specPath) ? `«${leanBase}.spec»` : undefined;
|
|
153
|
+
let { typesFile, defFile } = transformModuleLean(typed, specImport, leanModuleOverride);
|
|
146
154
|
if (typesFile)
|
|
147
155
|
typesFile = peepholeModule(typesFile, "lean");
|
|
148
156
|
defFile = peepholeModule(defFile, "lean");
|
|
149
|
-
const typesPath = typesFile ? path.join(dir, `${
|
|
157
|
+
const typesPath = typesFile ? path.join(dir, `${leanBase}.types.lean`) : null;
|
|
150
158
|
const typesText = typesFile ? emitLeanFile(typesFile) : null;
|
|
151
|
-
const defPath = path.join(dir, `${
|
|
159
|
+
const defPath = path.join(dir, `${leanBase}.def.lean`);
|
|
152
160
|
const defText = emitLeanFile(defFile);
|
|
153
161
|
if (cmd === "gen") {
|
|
154
162
|
leanGen(typesPath, defPath, typesText, defText);
|
|
@@ -156,7 +164,7 @@ function main() {
|
|
|
156
164
|
}
|
|
157
165
|
if (cmd === "check") {
|
|
158
166
|
leanGen(typesPath, defPath, typesText, defText);
|
|
159
|
-
if (!leanCheck(dir,
|
|
167
|
+
if (!leanCheck(dir, leanBase))
|
|
160
168
|
process.exit(1);
|
|
161
169
|
return;
|
|
162
170
|
}
|
package/tools/dist/narrow.js
CHANGED
|
@@ -100,7 +100,7 @@ const parseSimpleOptionalCheck = parseOptionalCheck;
|
|
|
100
100
|
// ── Walkers ──────────────────────────────────────────────────
|
|
101
101
|
function walkExpr(e) {
|
|
102
102
|
const r = recurseExpr(e);
|
|
103
|
-
return ruleNullish(r) ?? ruleOptChain(r) ?? ruleImplOptional(r) ?? ruleImplArrayIsArray(r) ?? ruleConditionalArrayIsArray(r) ?? ruleConditionalAndArrayIsArray(r) ?? ruleConditionalAndOptional(r) ?? ruleConditionalOptionalSimple(r) ?? ruleConditionalInMap(r) ?? ruleConditionalOptionalTruthy(r) ?? r;
|
|
103
|
+
return ruleNullish(r) ?? ruleNullishIndex(r) ?? ruleOptChainIndex(r) ?? ruleOptChain(r) ?? ruleImplOptional(r) ?? ruleImplArrayIsArray(r) ?? ruleConditionalArrayIsArray(r) ?? ruleConditionalAndArrayIsArray(r) ?? ruleConditionalAndOptional(r) ?? ruleConditionalOptionalSimple(r) ?? ruleConditionalInMap(r) ?? ruleConditionalOptionalTruthy(r) ?? r;
|
|
104
104
|
}
|
|
105
105
|
function recurseExpr(e) {
|
|
106
106
|
const re = walkExpr;
|
|
@@ -144,7 +144,7 @@ function walkStmt(s) {
|
|
|
144
144
|
// array rule fires; independent narrows commute, so the order is harmless.)
|
|
145
145
|
// && rules fire before the simple rule because they produce nested ifs whose
|
|
146
146
|
// inner shape doesn't match the simple rule directly.
|
|
147
|
-
return ruleIfAndOptional(r) ?? ruleIfAndArrayIsArray(r) ?? ruleIfOptionalSimple(r) ?? r;
|
|
147
|
+
return ruleIfAndOptional(r) ?? ruleIfAndArrayIsArray(r) ?? ruleIfOptionalSimple(r) ?? ruleExprStmtAndOptional(r) ?? r;
|
|
148
148
|
}
|
|
149
149
|
function walkStmts(stmts) {
|
|
150
150
|
const result = [];
|
|
@@ -434,6 +434,61 @@ function ruleNullish(e) {
|
|
|
434
434
|
ty: e.ty,
|
|
435
435
|
};
|
|
436
436
|
}
|
|
437
|
+
/** Rule (expression): `arr[i] ?? right` — nullish coalescing on an array index.
|
|
438
|
+
* Under noUncheckedIndexedAccess `arr[i]` is `T | undefined`, undefined exactly
|
|
439
|
+
* when out of bounds, so → `(0 <= i && i < arr.length) ? arr[i] : right`. The
|
|
440
|
+
* guarded `then` keeps the seq index in bounds for the backend. (Map index is
|
|
441
|
+
* already optional-typed and handled by ruleNullish above; this is the array
|
|
442
|
+
* case, whose element type stays non-optional in expression position.) */
|
|
443
|
+
function ruleNullishIndex(e) {
|
|
444
|
+
if (e.kind !== "nullish")
|
|
445
|
+
return null;
|
|
446
|
+
if (e.left.kind !== "index")
|
|
447
|
+
return null;
|
|
448
|
+
if (e.left.obj.ty.kind !== "array")
|
|
449
|
+
return null;
|
|
450
|
+
const idx = e.left.idx;
|
|
451
|
+
const len = { kind: "field", obj: e.left.obj, field: "length", ty: { kind: "int" } };
|
|
452
|
+
const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
|
|
453
|
+
const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
|
|
454
|
+
const cond = { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
|
|
455
|
+
return { kind: "conditional", cond, then: e.left, else: e.right, ty: e.ty };
|
|
456
|
+
}
|
|
457
|
+
/** Rule (expression): `arr[i]?.<chain>` — optional chaining on an array index,
|
|
458
|
+
* the optChain sibling of ruleNullishIndex. `arr[i]` is `T | undefined`,
|
|
459
|
+
* undefined exactly out of bounds, so → `(0 <= i && i < arr.length) ? <chain on
|
|
460
|
+
* arr[i]> : undefined`. The conditional's optional type makes transform wrap the
|
|
461
|
+
* in-bounds chain result in Some and the OOB branch in None — the same Option<…>
|
|
462
|
+
* a directly-optional scrutinee yields via ruleOptChain, just bounds-guarded.
|
|
463
|
+
* (ruleOptChain itself bails here: an array index is typed as the non-optional
|
|
464
|
+
* element type, so its `?.` never reaches that rule.) */
|
|
465
|
+
function ruleOptChainIndex(e) {
|
|
466
|
+
if (e.kind !== "optChain")
|
|
467
|
+
return null;
|
|
468
|
+
if (e.obj.kind !== "index")
|
|
469
|
+
return null;
|
|
470
|
+
if (e.obj.obj.ty.kind !== "array")
|
|
471
|
+
return null;
|
|
472
|
+
const idx = e.obj.idx;
|
|
473
|
+
const len = { kind: "field", obj: e.obj.obj, field: "length", ty: { kind: "int" } };
|
|
474
|
+
const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
|
|
475
|
+
const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
|
|
476
|
+
const cond = { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
|
|
477
|
+
let body = e.obj; // arr[i] — in bounds under `cond`
|
|
478
|
+
for (const step of e.chain) {
|
|
479
|
+
if (step.kind === "field") {
|
|
480
|
+
body = { kind: "field", obj: body, field: step.name, ty: step.ty };
|
|
481
|
+
}
|
|
482
|
+
else if (step.kind === "index") {
|
|
483
|
+
body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const undef = { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
490
|
+
return { kind: "conditional", cond, then: body, else: undef, ty: e.ty };
|
|
491
|
+
}
|
|
437
492
|
/** Rule (expression): `obj?.<chain>` — single-eval optional chain.
|
|
438
493
|
* → `someMatch obj { Some(_oc{N}_val) => apply(chain, _oc{N}_val), None => undefined }`.
|
|
439
494
|
* The someBody applies the chain to the binder directly (field/call/index),
|
|
@@ -598,6 +653,38 @@ function ruleIfAndOptional(s) {
|
|
|
598
653
|
noneBody: [],
|
|
599
654
|
};
|
|
600
655
|
}
|
|
656
|
+
/** Rule: a bare expression statement `x !== undefined && rest` (the `if`-less
|
|
657
|
+
* guard idiom, TS-equivalent to `if (x !== undefined) rest;`) where `x` is a
|
|
658
|
+
* pure access path.
|
|
659
|
+
* → `someMatch x { Some(_x_val) => rest;, None => {} }`.
|
|
660
|
+
* Runs `rest` for effect inside the narrowed scope. Wrapping `rest` as an
|
|
661
|
+
* expr-statement and walking it lets chained checks
|
|
662
|
+
* (`a !== undefined && a.b !== undefined && a.b.f()`) nest into someMatches.
|
|
663
|
+
* Unlike the ternary rule (`ruleConditionalAndOptional`), a method call in
|
|
664
|
+
* `rest` is fine here: a statement-level someMatch arm keeps it in statement
|
|
665
|
+
* position, so transform never ANF-lifts it out of the arm (which would drop
|
|
666
|
+
* the guard and reference the un-narrowed optional). */
|
|
667
|
+
function ruleExprStmtAndOptional(s) {
|
|
668
|
+
if (s.kind !== "expr")
|
|
669
|
+
return null;
|
|
670
|
+
if (s.expr.kind !== "binop" || s.expr.op !== "&&")
|
|
671
|
+
return null;
|
|
672
|
+
const extracted = extractLeftmostOptionalCheck(s.expr);
|
|
673
|
+
if (!extracted)
|
|
674
|
+
return null;
|
|
675
|
+
const { check, restCond } = extracted;
|
|
676
|
+
const innerStmt = { kind: "expr", expr: restCond };
|
|
677
|
+
const someBody = canBeFalsy(check)
|
|
678
|
+
? [{ kind: "if", cond: bound(check), then: [walkStmt(innerStmt)], else: [] }]
|
|
679
|
+
: [walkStmt(innerStmt)];
|
|
680
|
+
return {
|
|
681
|
+
kind: "someMatch",
|
|
682
|
+
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
683
|
+
binder: check.binderHint,
|
|
684
|
+
someBody,
|
|
685
|
+
noneBody: [],
|
|
686
|
+
};
|
|
687
|
+
}
|
|
601
688
|
// ── Discriminant narrowing ──────────────────────────────────
|
|
602
689
|
/** Detect `Array.isArray(<path>)` where `<path>` is a var or a chain of
|
|
603
690
|
* field accesses rooted at a var, and the path's type is a synthesized
|
package/tools/dist/peephole.js
CHANGED
|
@@ -13,6 +13,7 @@ function mapExpr(e, f) {
|
|
|
13
13
|
case "emptyMap":
|
|
14
14
|
case "emptySet":
|
|
15
15
|
case "havoc":
|
|
16
|
+
case "default":
|
|
16
17
|
case "mapLiteral": return e;
|
|
17
18
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
18
19
|
case "unop": return { ...e, expr: r(e.expr) };
|
|
@@ -358,6 +359,7 @@ function rewriteChildrenExpr(e) {
|
|
|
358
359
|
case "emptyMap":
|
|
359
360
|
case "emptySet":
|
|
360
361
|
case "havoc":
|
|
362
|
+
case "default":
|
|
361
363
|
case "mapLiteral": return e;
|
|
362
364
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
363
365
|
case "unop": return { ...e, expr: r(e.expr) };
|