lemmascript 0.5.5 → 0.5.7
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/package.json +2 -1
- package/tools/dist/dafny-commands.js +3 -1
- package/tools/dist/dafny-emit.js +47 -5
- package/tools/dist/extract.js +73 -31
- package/tools/dist/guard-command.js +238 -0
- package/tools/dist/lean-emit.js +29 -6
- package/tools/dist/narrow.js +30 -6
- package/tools/dist/resolve.js +13 -1
- package/tools/dist/specparser.js +37 -15
- package/tools/dist/transform.js +193 -47
- package/tools/dist/emit.js +0 -253
package/tools/dist/transform.js
CHANGED
|
@@ -158,6 +158,24 @@ function isNat(ty) { return ty.kind === "nat"; }
|
|
|
158
158
|
function isIntegral(ty) { return ty.kind === "int" || ty.kind === "nat"; }
|
|
159
159
|
function isArray(ty) { return ty.kind === "array"; }
|
|
160
160
|
function isUser(ty) { return ty.kind === "user"; }
|
|
161
|
+
/** Truthiness test for a *lowered* value of source type `ty`, used by `||`
|
|
162
|
+
* falsiness lowering. Mirrors narrow.ts's `canBeFalsy`: only int/nat/string/bool
|
|
163
|
+
* values can be falsy in JS (`0`, `""`, `false`); every other value (array, user
|
|
164
|
+
* type, …) is always truthy. Returns null for the always-truthy types so callers
|
|
165
|
+
* can unwrap directly instead of emitting a redundant guard. */
|
|
166
|
+
function valueTruthyCond(value, ty) {
|
|
167
|
+
switch (ty.kind) {
|
|
168
|
+
case "int":
|
|
169
|
+
case "nat":
|
|
170
|
+
return { kind: "binop", op: "≠", left: value, right: { kind: "num", value: 0 } };
|
|
171
|
+
case "string":
|
|
172
|
+
return { kind: "binop", op: ">", left: { kind: "field", obj: value, field: "length" }, right: { kind: "num", value: 0 } };
|
|
173
|
+
case "bool":
|
|
174
|
+
return value;
|
|
175
|
+
default:
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
161
179
|
/** Check if transformed lambda body contains monadic binds. */
|
|
162
180
|
function isMonadicBody(stmts) {
|
|
163
181
|
for (const s of stmts) {
|
|
@@ -182,7 +200,7 @@ function isMonadicBody(stmts) {
|
|
|
182
200
|
const OP_MAP = {
|
|
183
201
|
"===": "=", "!==": "≠", ">=": "≥", "<=": "≤", ">": ">", "<": "<",
|
|
184
202
|
"&&": "∧", "||": "∨", "+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
|
|
185
|
-
"==": "=", "!=": "≠",
|
|
203
|
+
"==": "=", "!=": "≠", "<==>": "↔",
|
|
186
204
|
};
|
|
187
205
|
/** Bool-valued operators (for code-level conditions needing Decidable). */
|
|
188
206
|
const BOOL_OP_MAP = {
|
|
@@ -275,6 +293,55 @@ function wrapOptionalBranch(expr, raw) {
|
|
|
275
293
|
return expr; // already Option<T>, don't double-wrap
|
|
276
294
|
return { kind: "constructor", name: "some", type: "Option", args: [expr] };
|
|
277
295
|
}
|
|
296
|
+
/** Lean needs `let mut` for any local that is later reassigned. A const local
|
|
297
|
+
* whose collection field is mutated (`b.items.push(v)` → `b := b.(items := …)`)
|
|
298
|
+
* becomes an assign in the lowered body, so scan for assign targets and flip
|
|
299
|
+
* matching lets to mutable. Harmless on Dafny (method locals are `var`); and an
|
|
300
|
+
* assigned let already forces a method, so purity is unaffected. */
|
|
301
|
+
function promoteAssignedLets(stmts) {
|
|
302
|
+
const assigned = new Set();
|
|
303
|
+
const collect = (ss) => {
|
|
304
|
+
for (const s of ss) {
|
|
305
|
+
if (s.kind === "assign")
|
|
306
|
+
assigned.add(s.target);
|
|
307
|
+
else if (s.kind === "if") {
|
|
308
|
+
collect(s.then);
|
|
309
|
+
collect(s.else);
|
|
310
|
+
}
|
|
311
|
+
else if (s.kind === "while" || s.kind === "forin")
|
|
312
|
+
collect(s.body);
|
|
313
|
+
else if (s.kind === "match")
|
|
314
|
+
s.arms.forEach(a => collect(a.body));
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
collect(stmts);
|
|
318
|
+
if (assigned.size === 0)
|
|
319
|
+
return stmts;
|
|
320
|
+
const fix = (ss) => ss.map(s => {
|
|
321
|
+
const s2 = s.kind === "let" && !s.mutable && assigned.has(s.name) ? { ...s, mutable: true } : s;
|
|
322
|
+
if (s2.kind === "if")
|
|
323
|
+
return { ...s2, then: fix(s2.then), else: fix(s2.else) };
|
|
324
|
+
if (s2.kind === "while" || s2.kind === "forin")
|
|
325
|
+
return { ...s2, body: fix(s2.body) };
|
|
326
|
+
if (s2.kind === "match")
|
|
327
|
+
return { ...s2, arms: s2.arms.map(a => ({ ...a, body: fix(a.body) })) };
|
|
328
|
+
return s2;
|
|
329
|
+
});
|
|
330
|
+
return fix(stmts);
|
|
331
|
+
}
|
|
332
|
+
/** Build a nested record-update assigning `newVal` to a field-path receiver
|
|
333
|
+
* rooted at a var: `b.a.items` → `b := b.(a := b.a.(items := newVal))`.
|
|
334
|
+
* Returns null if the path isn't a chain of field accesses ending at a var. */
|
|
335
|
+
function buildNestedFieldUpdate(recv, newVal) {
|
|
336
|
+
if (recv.kind !== "field")
|
|
337
|
+
return null;
|
|
338
|
+
const upd = { kind: "record", spread: lowerExpr(recv.obj, null), fields: [{ name: recv.field, value: newVal }] };
|
|
339
|
+
if (recv.obj.kind === "var")
|
|
340
|
+
return { root: recv.obj.name, value: upd };
|
|
341
|
+
if (recv.obj.kind === "field")
|
|
342
|
+
return buildNestedFieldUpdate(recv.obj, upd);
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
278
345
|
function lowerExpr(e, binds) {
|
|
279
346
|
// Monadic lifting: extract embedded method calls to let-binds.
|
|
280
347
|
// `callKind: "method"` means a global var-fn call (classifyCall returns
|
|
@@ -301,13 +368,17 @@ function lowerExpr(e, binds) {
|
|
|
301
368
|
// String truthiness: !str → str == ""
|
|
302
369
|
if (e.op === "!" && e.expr.ty.kind === "string")
|
|
303
370
|
return { kind: "binop", op: "=", left: lowerExpr(e.expr, binds), right: { kind: "str", value: "" } };
|
|
304
|
-
// Optional truthiness: !opt →
|
|
371
|
+
// Optional truthiness: !opt → None negates to `true`. The Some branch is
|
|
372
|
+
// `!(value truthy)`: always-truthy inners (array/user) give a plain `false`,
|
|
373
|
+
// while falsy-capable inners re-test the wrapped value (`!Some(0)` is `true`).
|
|
374
|
+
// Mirrors the `||` falsiness rule.
|
|
305
375
|
if (e.op === "!" && e.expr.ty.kind === "optional") {
|
|
306
376
|
const bound = matchBinder("value");
|
|
377
|
+
const truthy = valueTruthyCond({ kind: "var", name: bound }, e.expr.ty.inner);
|
|
307
378
|
return {
|
|
308
379
|
kind: "match", scrutinee: lowerExpr(e.expr, binds),
|
|
309
380
|
arms: [
|
|
310
|
-
{ pattern: `.some ${bound}`, body: { kind: "bool", value: false } },
|
|
381
|
+
{ pattern: `.some ${bound}`, body: truthy ? { kind: "unop", op: "¬", expr: truthy } : { kind: "bool", value: false } },
|
|
311
382
|
{ pattern: ".none", body: { kind: "bool", value: true } },
|
|
312
383
|
],
|
|
313
384
|
};
|
|
@@ -383,37 +454,76 @@ function lowerExpr(e, binds) {
|
|
|
383
454
|
],
|
|
384
455
|
};
|
|
385
456
|
}
|
|
386
|
-
// || undefined on optional → identity (no-op: x || undefined = x)
|
|
457
|
+
// || undefined on optional → identity (no-op: x || undefined = x) when the
|
|
458
|
+
// inner type is always truthy. When it can be falsy, JS still drops the
|
|
459
|
+
// wrapped value: `Some(0) || undefined === undefined`, so the Some arm
|
|
460
|
+
// re-tests and falls back to None.
|
|
387
461
|
if (e.op === "||" && e.left.ty.kind === "optional" &&
|
|
388
462
|
e.right.kind === "var" && e.right.name === "undefined") {
|
|
389
|
-
|
|
463
|
+
const optExpr = lowerExpr(e.left, binds);
|
|
464
|
+
const bound = matchBinder("value");
|
|
465
|
+
const truthy = valueTruthyCond({ kind: "var", name: bound }, e.left.ty.inner);
|
|
466
|
+
if (!truthy)
|
|
467
|
+
return optExpr;
|
|
468
|
+
return {
|
|
469
|
+
kind: "match", scrutinee: optExpr,
|
|
470
|
+
arms: [
|
|
471
|
+
{ pattern: `.some ${bound}`, body: {
|
|
472
|
+
kind: "if", cond: truthy,
|
|
473
|
+
then: { kind: "app", fn: "Some", args: [{ kind: "var", name: bound }] },
|
|
474
|
+
else: { kind: "var", name: "undefined" }
|
|
475
|
+
} },
|
|
476
|
+
{ pattern: ".none", body: { kind: "var", name: "undefined" } },
|
|
477
|
+
],
|
|
478
|
+
};
|
|
390
479
|
}
|
|
391
|
-
// || on optional → match Some/None with default
|
|
480
|
+
// || on optional → match Some/None with default. JS `||` tests falsiness of
|
|
481
|
+
// the *unwrapped* value, so when the inner type can be falsy the Some arm must
|
|
482
|
+
// re-test (`Some(0) || 1 === 1`); array/user inners are always truthy and
|
|
483
|
+
// unwrap directly. Mirrors narrow.ts's canBeFalsy gate.
|
|
392
484
|
if (e.op === "||" && e.left.ty.kind === "optional") {
|
|
393
485
|
const optExpr = lowerExpr(e.left, binds);
|
|
394
486
|
const defaultExpr = lowerExpr(e.right, binds);
|
|
395
487
|
const bound = matchBinder("value");
|
|
488
|
+
const truthy = valueTruthyCond({ kind: "var", name: bound }, e.left.ty.inner);
|
|
489
|
+
const someBody = truthy
|
|
490
|
+
? { kind: "if", cond: truthy, then: { kind: "var", name: bound }, else: defaultExpr }
|
|
491
|
+
: { kind: "var", name: bound };
|
|
396
492
|
return {
|
|
397
493
|
kind: "match", scrutinee: optExpr,
|
|
398
494
|
arms: [
|
|
399
|
-
{ pattern: `.some ${bound}`, body:
|
|
495
|
+
{ pattern: `.some ${bound}`, body: someBody },
|
|
400
496
|
{ pattern: ".none", body: defaultExpr },
|
|
401
497
|
],
|
|
402
498
|
};
|
|
403
499
|
}
|
|
404
|
-
// || on map index → if key in map then map[key] else default
|
|
500
|
+
// || on map index → if key in map then map[key] else default. The stored
|
|
501
|
+
// value is still subject to JS falsiness (`counts.get(k) || 1` returns 1 when
|
|
502
|
+
// the stored value is 0), so for falsy-capable value types the present branch
|
|
503
|
+
// re-tests the value too. Always-truthy value types unwrap directly.
|
|
405
504
|
if (e.op === "||" && e.left.kind === "index" && e.left.obj.ty.kind === "map") {
|
|
406
505
|
const map = lowerExpr(e.left.obj, binds);
|
|
407
506
|
const key = lowerExpr(e.left.idx, binds);
|
|
408
507
|
const right = lowerExpr(e.right, binds);
|
|
508
|
+
const got = { kind: "index", arr: map, idx: key };
|
|
509
|
+
const truthy = valueTruthyCond(got, e.left.obj.ty.value);
|
|
409
510
|
return {
|
|
410
511
|
kind: "if",
|
|
411
512
|
cond: { kind: "binop", op: "in", left: key, right: map },
|
|
412
|
-
then: { kind: "
|
|
513
|
+
then: truthy ? { kind: "if", cond: truthy, then: got, else: right } : got,
|
|
514
|
+
else: right,
|
|
413
515
|
};
|
|
414
516
|
}
|
|
415
|
-
// || on non-optional
|
|
416
|
-
|
|
517
|
+
// || on non-optional array → `xs` itself: every array (even `[]`) is truthy
|
|
518
|
+
// in JS, so `xs || ys` short-circuits to `xs` and `ys` is never evaluated.
|
|
519
|
+
// resolve types the whole `||` as the array, so any optional context (e.g.
|
|
520
|
+
// `xs || undefined`) gets its single Some-wrap from the standard coercion at
|
|
521
|
+
// the use site — this rule must not add one. Mirrors the `!array` rule above.
|
|
522
|
+
if (e.op === "||" && e.left.ty.kind === "array") {
|
|
523
|
+
return lowerExpr(e.left, binds);
|
|
524
|
+
}
|
|
525
|
+
// || on non-optional string/user → if non-empty then x else default
|
|
526
|
+
if (e.op === "||" && (e.left.ty.kind === "string" ||
|
|
417
527
|
(e.left.ty.kind === "user" && e.right.ty.kind === "string"))) {
|
|
418
528
|
const left = lowerExpr(e.left, binds);
|
|
419
529
|
const right = lowerExpr(e.right, binds);
|
|
@@ -421,8 +531,8 @@ function lowerExpr(e, binds) {
|
|
|
421
531
|
const rightIsUndef = e.right.kind === "var" && e.right.name === "undefined";
|
|
422
532
|
return {
|
|
423
533
|
kind: "if",
|
|
424
|
-
// strings carry the `length` marker
|
|
425
|
-
// in
|
|
534
|
+
// strings carry the `length` marker — it renders to `|x|` in Dafny and
|
|
535
|
+
// `.length` in Lean (whose String has no `.size` field).
|
|
426
536
|
cond: { kind: "binop", op: ">", left: { kind: "field", obj: left, field: e.left.ty.kind === "string" ? "length" : "size" }, right: { kind: "num", value: 0 } },
|
|
427
537
|
then: rightIsUndef ? { kind: "app", fn: "Some", args: [left] } : left,
|
|
428
538
|
else: right,
|
|
@@ -442,20 +552,43 @@ function lowerExpr(e, binds) {
|
|
|
442
552
|
else: { kind: "var", name: "undefined" },
|
|
443
553
|
};
|
|
444
554
|
}
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
return { kind: "
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
555
|
+
// String concatenation: `+` with a string operand. Stringify int/nat
|
|
556
|
+
// operands (Dafny NatToString, Lean toString) and join with arrayConcat
|
|
557
|
+
// (rendered `+` in Dafny, `++` in Lean).
|
|
558
|
+
if (e.op === "+" && (e.left.ty.kind === "string" || e.right.ty.kind === "string")) {
|
|
559
|
+
const strify = (o) => {
|
|
560
|
+
if (o.ty.kind !== "int" && o.ty.kind !== "nat")
|
|
561
|
+
return lowerExpr(o, binds);
|
|
562
|
+
// Lean `toString` handles any Int; Dafny needs IntToString for signed
|
|
563
|
+
// ints (NatToString is nat-only).
|
|
564
|
+
const fn = _opts.backend !== "dafny" ? "ToString" : o.ty.kind === "nat" ? "NatToString" : "IntToString";
|
|
565
|
+
return { kind: "app", fn, args: [lowerExpr(o, binds)] };
|
|
566
|
+
};
|
|
567
|
+
return { kind: "binop", op: "arrayConcat", left: strify(e.left), right: strify(e.right) };
|
|
568
|
+
}
|
|
569
|
+
// JS `%` is truncated (sign of the dividend); a signed `int` differs from the
|
|
570
|
+
// Euclidean `%` of Dafny/Lean, so route it through JSRem (Lean: `Int.tmod`).
|
|
571
|
+
if (e.op === "%" && e.left.ty.kind === "int") {
|
|
572
|
+
return { kind: "app", fn: "JSRem", args: [lowerExpr(e.left, binds), lowerExpr(e.right, binds)] };
|
|
573
|
+
}
|
|
574
|
+
// JS bigint `/` truncates toward zero (`-3n / 2n === -1n`); it differs from the
|
|
575
|
+
// floored `/` of Dafny/Lean, so route it through JSTruncDiv (Lean: `Int.tdiv`).
|
|
576
|
+
if (e.op === "/" && e.ty.kind === "int") {
|
|
577
|
+
return { kind: "app", fn: "JSTruncDiv", args: [lowerExpr(e.left, binds), lowerExpr(e.right, binds)] };
|
|
578
|
+
}
|
|
579
|
+
// JS string ordering is lexicographic vs Dafny's seq prefix order, so route
|
|
580
|
+
// through JSStringLt. Dafny-only: Lean's native `<` is already lexicographic.
|
|
581
|
+
if (_opts.backend === "dafny" && ["<", "<=", ">", ">="].includes(e.op) && e.left.ty.kind === "string") {
|
|
582
|
+
const l = lowerExpr(e.left, binds), r = lowerExpr(e.right, binds);
|
|
583
|
+
const lt = (x, y) => ({ kind: "app", fn: "JSStringLt", args: [x, y] });
|
|
584
|
+
const not = (x) => ({ kind: "unop", op: "¬", expr: x });
|
|
585
|
+
if (e.op === "<")
|
|
586
|
+
return lt(l, r);
|
|
587
|
+
if (e.op === ">")
|
|
588
|
+
return lt(r, l);
|
|
589
|
+
if (e.op === "<=")
|
|
590
|
+
return not(lt(r, l));
|
|
591
|
+
return not(lt(l, r)); // >=
|
|
459
592
|
}
|
|
460
593
|
// Numeric int→real coercion. After resolve, `/` is always real, and any
|
|
461
594
|
// arithmetic/comparison mixing real and integral operands is real-valued.
|
|
@@ -571,14 +704,21 @@ function lowerExpr(e, binds) {
|
|
|
571
704
|
let method = e.fn.field;
|
|
572
705
|
const args = e.args.map((a, i) => {
|
|
573
706
|
const lowered = lowerExpr(a, binds);
|
|
574
|
-
//
|
|
575
|
-
|
|
707
|
+
// Array index args must be nat in Lean: `with`'s index (0), includes/indexOf `from` (1).
|
|
708
|
+
const isArrIdxArg = e.fn.kind === "field" && e.fn.obj.ty.kind === "array" &&
|
|
709
|
+
((e.fn.field === "with" && i === 0) || ((e.fn.field === "includes" || e.fn.field === "indexOf") && i === 1));
|
|
710
|
+
if (isArrIdxArg && !isNat(a.ty))
|
|
576
711
|
return { kind: "toNat", expr: lowered };
|
|
577
712
|
return lowered;
|
|
578
713
|
});
|
|
579
|
-
// arr.concat(
|
|
580
|
-
if (method === "concat" && e.fn.obj.ty.kind === "array"
|
|
581
|
-
|
|
714
|
+
// arr.concat(...args): each array arg is spread, each value arg appended.
|
|
715
|
+
if (method === "concat" && e.fn.obj.ty.kind === "array") {
|
|
716
|
+
let acc = recv;
|
|
717
|
+
for (let k = 0; k < args.length; k++) {
|
|
718
|
+
const piece = e.args[k].ty.kind === "array" ? args[k] : { kind: "arrayLiteral", elems: [args[k]] };
|
|
719
|
+
acc = { kind: "binop", op: "arrayConcat", left: acc, right: piece };
|
|
720
|
+
}
|
|
721
|
+
return acc;
|
|
582
722
|
}
|
|
583
723
|
// Spec-context map get: result type is non-optional → direct access
|
|
584
724
|
if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
|
|
@@ -1212,18 +1352,24 @@ function transformStmt(s, typeDecls) {
|
|
|
1212
1352
|
case "break": return [{ kind: "break" }];
|
|
1213
1353
|
case "continue": return [{ kind: "continue" }];
|
|
1214
1354
|
case "expr": {
|
|
1215
|
-
// Mutating collection call: m.set(k, v) → m := m.set(k, v)
|
|
1216
|
-
//
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
(
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1355
|
+
// Mutating collection call: m.set(k, v) → m := m.set(k, v) (same for set
|
|
1356
|
+
// .add/.delete and array .push). The receiver may be a bare var, or a
|
|
1357
|
+
// field path rooted at a var (b.items.push(v) → b := b.(items := b.items + [v])).
|
|
1358
|
+
if (s.expr.kind === "call" && s.expr.fn.kind === "field") {
|
|
1359
|
+
const recv = s.expr.fn.obj;
|
|
1360
|
+
const f = s.expr.fn.field;
|
|
1361
|
+
const isMutating = ((recv.ty.kind === "map" || recv.ty.kind === "set") && (f === "set" || f === "add" || f === "delete")) ||
|
|
1362
|
+
(recv.ty.kind === "array" && f === "push");
|
|
1363
|
+
if (isMutating && recv.kind === "var") {
|
|
1364
|
+
const { binds, expr } = liftMethodCalls(s.expr);
|
|
1365
|
+
return [...binds, { kind: "assign", target: recv.name, value: expr }];
|
|
1366
|
+
}
|
|
1367
|
+
if (isMutating && recv.kind === "field") {
|
|
1368
|
+
const { binds, expr } = liftMethodCalls(s.expr);
|
|
1369
|
+
const upd = buildNestedFieldUpdate(recv, expr);
|
|
1370
|
+
if (upd)
|
|
1371
|
+
return [...binds, { kind: "assign", target: upd.root, value: upd.value }];
|
|
1372
|
+
}
|
|
1227
1373
|
}
|
|
1228
1374
|
// Optional chaining on map.get at statement level: m.get(k)?.push(v)
|
|
1229
1375
|
// → if k in m { m[k] := m[k] + [v] } (actual mutation, not value-discard).
|
|
@@ -1826,7 +1972,7 @@ export function transformModule(mod, specImport) {
|
|
|
1826
1972
|
else if (fn.forcePure) {
|
|
1827
1973
|
// //@ pure but body can't be auto-converted — emit function by method
|
|
1828
1974
|
_forofCounters.clear();
|
|
1829
|
-
const methodBody = transformStmts(fn.body, mod.typeDecls);
|
|
1975
|
+
const methodBody = promoteAssignedLets(transformStmts(fn.body, mod.typeDecls));
|
|
1830
1976
|
defByMethods.push({
|
|
1831
1977
|
kind: "def-by-method",
|
|
1832
1978
|
name: fn.name,
|
|
@@ -1888,7 +2034,7 @@ export function transformModule(mod, specImport) {
|
|
|
1888
2034
|
_forofCounters.clear();
|
|
1889
2035
|
let body = pureDefNames.has(fn.name)
|
|
1890
2036
|
? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
|
|
1891
|
-
: transformStmts(fn.body, mod.typeDecls);
|
|
2037
|
+
: promoteAssignedLets(transformStmts(fn.body, mod.typeDecls));
|
|
1892
2038
|
// Shadow reassigned parameters with mutable locals
|
|
1893
2039
|
const paramNames = new Set(fn.params.map(p => p.name));
|
|
1894
2040
|
const reassigned = findReassignedNames(fn.body, paramNames);
|
|
@@ -1915,7 +2061,7 @@ export function transformModule(mod, specImport) {
|
|
|
1915
2061
|
const classMethods = cls.methods.map(fn => {
|
|
1916
2062
|
const ensures = fn.ensures.map(transformExpr);
|
|
1917
2063
|
_forofCounters.clear();
|
|
1918
|
-
const body = transformStmts(fn.body, mod.typeDecls);
|
|
2064
|
+
const body = promoteAssignedLets(transformStmts(fn.body, mod.typeDecls));
|
|
1919
2065
|
return {
|
|
1920
2066
|
kind: "method",
|
|
1921
2067
|
name: fn.name,
|
package/tools/dist/emit.js
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lean IR → text. Trivial pretty-printer.
|
|
3
|
-
* No logic, no type decisions — just serialization.
|
|
4
|
-
*/
|
|
5
|
-
// ── Lean keyword escaping ────────────────────────────────────
|
|
6
|
-
const LEAN_KEYWORDS = new Set([
|
|
7
|
-
"def", "theorem", "lemma", "example", "structure", "class", "instance",
|
|
8
|
-
"inductive", "where", "match", "with", "if", "then", "else", "do",
|
|
9
|
-
"let", "mut", "return", "for", "in", "while", "break", "continue",
|
|
10
|
-
"import", "open", "section", "namespace", "end", "set_option",
|
|
11
|
-
"variable", "axiom", "constant", "private", "protected", "noncomputable",
|
|
12
|
-
"partial", "unsafe", "macro", "syntax", "by", "fun", "have", "show",
|
|
13
|
-
"at", "from", "to", "deriving", "extends", "true", "false",
|
|
14
|
-
]);
|
|
15
|
-
function escapeName(name) {
|
|
16
|
-
return LEAN_KEYWORDS.has(name) ? `«${name}»` : name;
|
|
17
|
-
}
|
|
18
|
-
// ── Operator precedence (for parenthesization) ──────────────
|
|
19
|
-
const PREC = {
|
|
20
|
-
"→": 1, "∨": 2, "∧": 3,
|
|
21
|
-
"=": 4, "≠": 4, "≥": 4, "≤": 4, ">": 4, "<": 4,
|
|
22
|
-
"+": 5, "-": 5, "*": 6, "/": 6, "%": 6,
|
|
23
|
-
};
|
|
24
|
-
function prec(op) { return PREC[op] ?? 10; }
|
|
25
|
-
// ── Expression emission ─────────────────────────────────────
|
|
26
|
-
function emitExpr(e, parentPrec) {
|
|
27
|
-
switch (e.kind) {
|
|
28
|
-
case "var": return escapeName(e.name);
|
|
29
|
-
case "num": return `${e.value}`;
|
|
30
|
-
case "bool": return e.value ? "true" : "false";
|
|
31
|
-
case "str": return `"${e.value}"`;
|
|
32
|
-
case "constructor": return `.${e.name}`;
|
|
33
|
-
case "arrayLiteral":
|
|
34
|
-
if (e.elems.length === 0)
|
|
35
|
-
return `#[]`;
|
|
36
|
-
return `#[${e.elems.map(el => emitExpr(el)).join(", ")}]`;
|
|
37
|
-
case "dotCall": {
|
|
38
|
-
const obj = emitExpr(e.obj);
|
|
39
|
-
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "dotCall";
|
|
40
|
-
const receiver = wrap ? `(${obj})` : obj;
|
|
41
|
-
const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
|
|
42
|
-
return args.length > 0 ? `${receiver}.${e.method} ${args.join(" ")}` : `${receiver}.${e.method}`;
|
|
43
|
-
}
|
|
44
|
-
case "lambda": {
|
|
45
|
-
const params = e.params.map(p => p.name).join(" ");
|
|
46
|
-
// Single return statement → expression lambda
|
|
47
|
-
if (e.body.length === 1 && e.body[0].kind === "return") {
|
|
48
|
-
return `(fun ${params} => ${emitExpr(e.body[0].value)})`;
|
|
49
|
-
}
|
|
50
|
-
// Multi-statement → do block
|
|
51
|
-
return `(fun ${params} => do\n${emitStmts(e.body, 2)})`;
|
|
52
|
-
}
|
|
53
|
-
case "unop":
|
|
54
|
-
if (e.op === "¬")
|
|
55
|
-
return `¬(${emitExpr(e.expr)})`;
|
|
56
|
-
if (e.op === "-" && e.expr.kind === "num")
|
|
57
|
-
return `-${e.expr.value}`;
|
|
58
|
-
return `(-${emitExpr(e.expr)})`;
|
|
59
|
-
case "binop": {
|
|
60
|
-
const s = `${emitExpr(e.left, prec(e.op))} ${e.op} ${emitExpr(e.right, prec(e.op))}`;
|
|
61
|
-
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
62
|
-
}
|
|
63
|
-
case "implies": {
|
|
64
|
-
const parts = [...e.premises.map(p => emitExpr(p)), emitExpr(e.conclusion)];
|
|
65
|
-
const s = parts.join(" → ");
|
|
66
|
-
return parentPrec !== undefined ? `(${s})` : s;
|
|
67
|
-
}
|
|
68
|
-
case "app": {
|
|
69
|
-
const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
|
|
70
|
-
return `${e.fn} ${args.join(" ")}`;
|
|
71
|
-
}
|
|
72
|
-
case "field": {
|
|
73
|
-
const obj = emitExpr(e.obj);
|
|
74
|
-
const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bool";
|
|
75
|
-
return wrap ? `(${obj}).${escapeName(e.field)}` : `${obj}.${escapeName(e.field)}`;
|
|
76
|
-
}
|
|
77
|
-
case "toNat": {
|
|
78
|
-
const inner = emitExpr(e.expr);
|
|
79
|
-
const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
|
|
80
|
-
return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
|
|
81
|
-
}
|
|
82
|
-
case "index":
|
|
83
|
-
return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
|
|
84
|
-
case "record": {
|
|
85
|
-
const fields = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
|
|
86
|
-
if (e.spread)
|
|
87
|
-
return `{ ${emitExpr(e.spread)} with ${fields.join(", ")} }`;
|
|
88
|
-
return `{ ${fields.join(", ")} }`;
|
|
89
|
-
}
|
|
90
|
-
case "if":
|
|
91
|
-
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
92
|
-
case "match": {
|
|
93
|
-
const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
|
|
94
|
-
return `match ${e.scrutinee} with ${arms.join(" ")}`;
|
|
95
|
-
}
|
|
96
|
-
case "forall": return `∀ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
|
|
97
|
-
case "exists": return `∃ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
|
|
98
|
-
case "let": return `let ${e.name} := ${emitExpr(e.value)}\n${emitExpr(e.body)}`;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// ── Statement emission ──────────────────────────────────────
|
|
102
|
-
function emitStmts(stmts, indent) {
|
|
103
|
-
const pad = " ".repeat(indent);
|
|
104
|
-
return stmts.map(s => emitStmt(s, indent)).join("\n");
|
|
105
|
-
}
|
|
106
|
-
function emitStmt(s, indent) {
|
|
107
|
-
const pad = " ".repeat(indent);
|
|
108
|
-
switch (s.kind) {
|
|
109
|
-
case "let":
|
|
110
|
-
return s.mutable
|
|
111
|
-
? `${pad}let mut ${escapeName(s.name)} : ${s.type} := ${emitExpr(s.value)}`
|
|
112
|
-
: `${pad}let ${escapeName(s.name)} := ${emitExpr(s.value)}`;
|
|
113
|
-
case "assign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
|
|
114
|
-
case "bind": return `${pad}${escapeName(s.target)} ← ${emitExpr(s.value)}`;
|
|
115
|
-
case "let-bind": return `${pad}let ${s.name} ← ${emitExpr(s.value)}`;
|
|
116
|
-
case "return": return `${pad}return ${emitExpr(s.value)}`;
|
|
117
|
-
case "break": return `${pad}break`;
|
|
118
|
-
case "continue": return `${pad}continue`;
|
|
119
|
-
case "if": {
|
|
120
|
-
let out = `${pad}if ${emitExpr(s.cond)} then\n${emitStmts(s.then, indent + 1)}`;
|
|
121
|
-
if (s.else.length > 0) {
|
|
122
|
-
if (s.else.length === 1 && s.else[0].kind === "if") {
|
|
123
|
-
const ei = s.else[0];
|
|
124
|
-
out += `\n${pad}else if ${emitExpr(ei.cond)} then\n${emitStmts(ei.then, indent + 1)}`;
|
|
125
|
-
if (ei.else.length > 0)
|
|
126
|
-
out += `\n${pad}else\n${emitStmts(ei.else, indent + 1)}`;
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
out += `\n${pad}else\n${emitStmts(s.else, indent + 1)}`;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return out;
|
|
133
|
-
}
|
|
134
|
-
case "match": {
|
|
135
|
-
const lines = [`${pad}match ${s.scrutinee} with`];
|
|
136
|
-
for (const arm of s.arms) {
|
|
137
|
-
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
138
|
-
lines.push(emitStmts(arm.body, indent + 1));
|
|
139
|
-
}
|
|
140
|
-
return lines.join("\n");
|
|
141
|
-
}
|
|
142
|
-
case "while": {
|
|
143
|
-
const lines = [`${pad}while ${emitExpr(s.cond)}`];
|
|
144
|
-
for (const inv of s.invariants)
|
|
145
|
-
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
146
|
-
if (s.doneWith)
|
|
147
|
-
lines.push(`${pad} done_with ${emitExpr(s.doneWith)}`);
|
|
148
|
-
if (s.decreasing)
|
|
149
|
-
lines.push(`${pad} decreasing ${emitExpr(s.decreasing)}`);
|
|
150
|
-
lines.push(`${pad}do`);
|
|
151
|
-
lines.push(emitStmts(s.body, indent + 1));
|
|
152
|
-
return lines.join("\n");
|
|
153
|
-
}
|
|
154
|
-
case "forin": {
|
|
155
|
-
const lines = [`${pad}for ${s.idx} in [:${emitExpr(s.bound)}]`];
|
|
156
|
-
for (const inv of s.invariants)
|
|
157
|
-
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
158
|
-
lines.push(`${pad}do`);
|
|
159
|
-
lines.push(emitStmts(s.body, indent + 1));
|
|
160
|
-
return lines.join("\n");
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
// ── Declaration emission ─────────────────────────────────────
|
|
165
|
-
function emitDecl(d) {
|
|
166
|
-
switch (d.kind) {
|
|
167
|
-
case "inductive": {
|
|
168
|
-
const lines = [`inductive ${d.name} where`];
|
|
169
|
-
for (const c of d.constructors) {
|
|
170
|
-
if (c.fields.length === 0) {
|
|
171
|
-
lines.push(` | ${c.name} : ${d.name}`);
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
const params = c.fields.map(f => `(${escapeName(f.name)} : ${f.type})`).join(" ");
|
|
175
|
-
lines.push(` | ${c.name} ${params} : ${d.name}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (d.deriving.length > 0)
|
|
179
|
-
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
180
|
-
return lines.join("\n");
|
|
181
|
-
}
|
|
182
|
-
case "structure": {
|
|
183
|
-
const lines = [`structure ${d.name} where`];
|
|
184
|
-
for (const f of d.fields)
|
|
185
|
-
lines.push(` ${escapeName(f.name)} : ${f.type}`);
|
|
186
|
-
if (d.deriving.length > 0)
|
|
187
|
-
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
188
|
-
return lines.join("\n");
|
|
189
|
-
}
|
|
190
|
-
case "def": {
|
|
191
|
-
const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
|
|
192
|
-
return `def ${d.name} ${params} : ${d.returnType} :=\n${emitPureExpr(d.body, 1)}`;
|
|
193
|
-
}
|
|
194
|
-
case "method": {
|
|
195
|
-
const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
|
|
196
|
-
const lines = [`method ${d.name} ${params} return (res : ${d.returnType})`];
|
|
197
|
-
for (const r of d.requires)
|
|
198
|
-
lines.push(` require ${emitExpr(r)}`);
|
|
199
|
-
for (const e of d.ensures)
|
|
200
|
-
lines.push(` ensures ${emitExpr(e)}`);
|
|
201
|
-
lines.push(" do");
|
|
202
|
-
lines.push(emitStmts(d.body, 2));
|
|
203
|
-
return lines.join("\n");
|
|
204
|
-
}
|
|
205
|
-
case "namespace": {
|
|
206
|
-
const lines = [`namespace ${d.name}`];
|
|
207
|
-
for (const inner of d.decls)
|
|
208
|
-
lines.push("", emitDecl(inner));
|
|
209
|
-
lines.push("", `end ${d.name}`);
|
|
210
|
-
return lines.join("\n");
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
/** Emit a pure expression with indented if/match blocks. */
|
|
215
|
-
function emitPureExpr(e, indent) {
|
|
216
|
-
const pad = " ".repeat(indent);
|
|
217
|
-
switch (e.kind) {
|
|
218
|
-
case "if":
|
|
219
|
-
return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
|
|
220
|
-
case "match": {
|
|
221
|
-
const lines = [`${pad}match ${e.scrutinee} with`];
|
|
222
|
-
for (const arm of e.arms) {
|
|
223
|
-
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
224
|
-
lines.push(emitPureExpr(arm.body, indent + 1));
|
|
225
|
-
}
|
|
226
|
-
return lines.join("\n");
|
|
227
|
-
}
|
|
228
|
-
case "let":
|
|
229
|
-
return `${pad}let ${e.name} := ${emitExpr(e.value)}\n${emitPureExpr(e.body, indent)}`;
|
|
230
|
-
default:
|
|
231
|
-
return `${pad}${emitExpr(e)}`;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
// ── File emission ────────────────────────────────────────────
|
|
235
|
-
export function emitFile(file) {
|
|
236
|
-
const lines = [];
|
|
237
|
-
if (file.comment) {
|
|
238
|
-
lines.push("/-");
|
|
239
|
-
lines.push(file.comment);
|
|
240
|
-
lines.push("-/");
|
|
241
|
-
}
|
|
242
|
-
for (const imp of file.imports)
|
|
243
|
-
lines.push(`import ${imp}`);
|
|
244
|
-
if (file.options.length > 0)
|
|
245
|
-
lines.push("");
|
|
246
|
-
for (const opt of file.options)
|
|
247
|
-
lines.push(`set_option ${opt.key} ${opt.value}`);
|
|
248
|
-
for (const decl of file.decls) {
|
|
249
|
-
lines.push("");
|
|
250
|
-
lines.push(emitDecl(decl));
|
|
251
|
-
}
|
|
252
|
-
return lines.join("\n") + "\n";
|
|
253
|
-
}
|