lemmascript 0.5.5 → 0.5.6
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 +1 -1
- package/tools/dist/dafny-commands.js +3 -1
- package/tools/dist/dafny-emit.js +47 -5
- package/tools/dist/extract.js +43 -30
- 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/package.json
CHANGED
|
@@ -28,7 +28,9 @@ export function dafnyCheckDiff(genPath, dfyPath) {
|
|
|
28
28
|
diff = typeof e.stdout === "string" ? e.stdout : e.stdout.toString("utf-8");
|
|
29
29
|
}
|
|
30
30
|
else {
|
|
31
|
-
|
|
31
|
+
// git couldn't be spawned: the check never ran, so fail loud, don't green-pass.
|
|
32
|
+
console.error(`ERROR: could not run \`git diff\` to verify ${path.basename(dfyPath)} is additions-only (is git installed?)`);
|
|
33
|
+
return false;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
const deletions = diff.split("\n").filter(l => l.startsWith("-") && !l.startsWith("---"));
|
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -86,7 +86,7 @@ function methodHeader(prefix, params, returnType) {
|
|
|
86
86
|
// ── Lean op → Dafny op ─────────────────────────────────────
|
|
87
87
|
const OP_MAP = {
|
|
88
88
|
"=": "==", "≠": "!=", "≥": ">=", "≤": "<=",
|
|
89
|
-
"∧": "&&", "∨": "||", "¬": "!",
|
|
89
|
+
"∧": "&&", "∨": "||", "¬": "!", "↔": "<==>",
|
|
90
90
|
"arrayConcat": "+",
|
|
91
91
|
};
|
|
92
92
|
function mapOp(op) { return OP_MAP[op] ?? op; }
|
|
@@ -152,15 +152,15 @@ function emitExpr(e) {
|
|
|
152
152
|
if (e.method === "with")
|
|
153
153
|
return `${obj}[${args[0]} := ${args[1]}]`;
|
|
154
154
|
if (e.method === "includes")
|
|
155
|
-
return `(${args[0]} in ${obj})`;
|
|
155
|
+
return args.length > 1 ? `(${args[0]} in ${obj}[${args[1]}..])` : `(${args[0]} in ${obj})`;
|
|
156
156
|
if (e.method === "indexOf") {
|
|
157
157
|
needPreamble("SeqIndexOf");
|
|
158
|
-
return `SeqIndexOf(${obj}, ${args[0]})`;
|
|
158
|
+
return args.length > 1 ? `SeqIndexOfFrom(${obj}, ${args[0]}, ${args[1]})` : `SeqIndexOf(${obj}, ${args[0]})`;
|
|
159
159
|
}
|
|
160
160
|
if (e.method === "push")
|
|
161
|
-
return `(${obj} + [${args
|
|
161
|
+
return `(${obj} + [${args.join(", ")}])`;
|
|
162
162
|
if (e.method === "concat")
|
|
163
|
-
return `(${obj} + [${args
|
|
163
|
+
return `(${obj} + [${args.join(", ")}])`;
|
|
164
164
|
// No-arg slice is a full copy; Dafny seq is an immutable value type, so
|
|
165
165
|
// the copy is just the seq itself (the idiom for "copy then mutate").
|
|
166
166
|
if (e.method === "slice" && args.length === 0)
|
|
@@ -399,12 +399,22 @@ function emitExpr(e) {
|
|
|
399
399
|
return `{${args.join(", ")}}`;
|
|
400
400
|
if (e.fn === "JSFloorDiv")
|
|
401
401
|
needPreamble("JSFloorDiv");
|
|
402
|
+
if (e.fn === "JSRem")
|
|
403
|
+
needPreamble("JSRem");
|
|
404
|
+
if (e.fn === "JSTruncDiv")
|
|
405
|
+
needPreamble("JSTruncDiv");
|
|
406
|
+
if (e.fn === "JSStringLt")
|
|
407
|
+
needPreamble("JSStringLt");
|
|
402
408
|
if (e.fn === "CeilReal")
|
|
403
409
|
needPreamble("CeilReal");
|
|
404
410
|
if (e.fn === "FloorReal")
|
|
405
411
|
needPreamble("FloorReal");
|
|
406
412
|
if (e.fn === "NatToString")
|
|
407
413
|
needPreamble("NatToString");
|
|
414
|
+
if (e.fn === "IntToString") {
|
|
415
|
+
needPreamble("NatToString");
|
|
416
|
+
needPreamble("IntToString");
|
|
417
|
+
}
|
|
408
418
|
if (e.fn === "MathAbs")
|
|
409
419
|
needPreamble("MathAbs");
|
|
410
420
|
if (e.fn === "MathMin")
|
|
@@ -421,6 +431,8 @@ function emitExpr(e) {
|
|
|
421
431
|
}
|
|
422
432
|
if (e.fn === "Perm")
|
|
423
433
|
needPreamble("Perm");
|
|
434
|
+
if (e.fn === "SetFromSeq")
|
|
435
|
+
needPreamble("SetFromSeq");
|
|
424
436
|
return `${escapeName(e.fn)}(${args.join(", ")})`;
|
|
425
437
|
}
|
|
426
438
|
case "field": {
|
|
@@ -804,6 +816,26 @@ const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
|
|
|
804
816
|
if a <= 0 then (-a) / (-b)
|
|
805
817
|
else -((a - 1) / (-b)) - 1
|
|
806
818
|
}`;
|
|
819
|
+
const JS_REM = `function JSRem(a: int, b: int): int
|
|
820
|
+
requires b != 0
|
|
821
|
+
{
|
|
822
|
+
var r := (if a < 0 then -a else a) % (if b < 0 then -b else b);
|
|
823
|
+
if a < 0 then -r else r
|
|
824
|
+
}`;
|
|
825
|
+
const JS_TRUNC_DIV = `function JSTruncDiv(a: int, b: int): int
|
|
826
|
+
requires b != 0
|
|
827
|
+
{
|
|
828
|
+
var q := (if a < 0 then -a else a) / (if b < 0 then -b else b);
|
|
829
|
+
if (a < 0) != (b < 0) then -q else q
|
|
830
|
+
}`;
|
|
831
|
+
const JS_STRING_LT = `predicate JSStringLt(s: string, t: string)
|
|
832
|
+
decreases |s|
|
|
833
|
+
{
|
|
834
|
+
if |s| == 0 then |t| > 0
|
|
835
|
+
else if |t| == 0 then false
|
|
836
|
+
else if s[0] != t[0] then s[0] < t[0]
|
|
837
|
+
else JSStringLt(s[1..], t[1..])
|
|
838
|
+
}`;
|
|
807
839
|
const FLOOR_REAL = `function FloorReal(x: real): int
|
|
808
840
|
{
|
|
809
841
|
x.Floor
|
|
@@ -1031,11 +1063,16 @@ const NAT_TO_STRING = `function NatToString(n: nat): string
|
|
|
1031
1063
|
if n < 10 then [digit]
|
|
1032
1064
|
else NatToString(n / 10) + [digit]
|
|
1033
1065
|
}`;
|
|
1066
|
+
const INT_TO_STRING = `function IntToString(n: int): string
|
|
1067
|
+
{
|
|
1068
|
+
if n < 0 then "-" + NatToString(-n) else NatToString(n)
|
|
1069
|
+
}`;
|
|
1034
1070
|
const MATH_ABS = `function MathAbs(x: int): nat { if x >= 0 then x else -x }`;
|
|
1035
1071
|
// perm(a, b) — `a` and `b` are reorderings of each other (equal as multisets).
|
|
1036
1072
|
// Transparent (Dafny unfolds it), so hand-proofs can reason with `multiset`
|
|
1037
1073
|
// directly. The `(==)` bound requires the element type to support equality.
|
|
1038
1074
|
const PERM = `predicate Perm<T(==)>(a: seq<T>, b: seq<T>) { multiset(a) == multiset(b) }`;
|
|
1075
|
+
const SET_FROM_SEQ = `function SetFromSeq<T(==)>(s: seq<T>): set<T> { set x | x in s }`;
|
|
1039
1076
|
const SET_TO_SEQ = `method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
|
|
1040
1077
|
ensures forall x :: x in s <==> x in res
|
|
1041
1078
|
ensures |res| == |s|
|
|
@@ -1063,6 +1100,9 @@ const PREAMBLE_CODE = [
|
|
|
1063
1100
|
["BitAnd", BIT_AND],
|
|
1064
1101
|
["BitOr", BIT_OR],
|
|
1065
1102
|
["JSFloorDiv", JS_FLOOR_DIV],
|
|
1103
|
+
["JSRem", JS_REM],
|
|
1104
|
+
["JSTruncDiv", JS_TRUNC_DIV],
|
|
1105
|
+
["JSStringLt", JS_STRING_LT],
|
|
1066
1106
|
["CeilReal", CEIL_REAL],
|
|
1067
1107
|
["FloorReal", FLOOR_REAL],
|
|
1068
1108
|
["SeqIndexOf", SEQ_INDEX_OF],
|
|
@@ -1078,12 +1118,14 @@ const PREAMBLE_CODE = [
|
|
|
1078
1118
|
["StringToLower", STRING_TO_LOWER],
|
|
1079
1119
|
["StringToUpper", STRING_TO_UPPER],
|
|
1080
1120
|
["NatToString", NAT_TO_STRING],
|
|
1121
|
+
["IntToString", INT_TO_STRING],
|
|
1081
1122
|
["MathAbs", MATH_ABS],
|
|
1082
1123
|
["MathMin", MATH_MIN],
|
|
1083
1124
|
["MathMax", MATH_MAX],
|
|
1084
1125
|
["MaxOfSeq", MAX_OF_SEQ],
|
|
1085
1126
|
["MinOfSeq", MIN_OF_SEQ],
|
|
1086
1127
|
["Perm", PERM],
|
|
1128
|
+
["SetFromSeq", SET_FROM_SEQ],
|
|
1087
1129
|
];
|
|
1088
1130
|
// ── Constructor and record helpers ───────────────────────────
|
|
1089
1131
|
let _recordCtors = new Map();
|
package/tools/dist/extract.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Produces structured AST nodes, not strings.
|
|
5
5
|
* The only strings are //@ annotation expressions (parsed later by specparser).
|
|
6
6
|
*/
|
|
7
|
-
import { Project, Node, SyntaxKind, ScriptTarget } from "ts-morph";
|
|
7
|
+
import { Project, Node, SyntaxKind, ScriptTarget, ts } from "ts-morph";
|
|
8
8
|
import { initTypeParser } from "./types.js";
|
|
9
9
|
// ── Expression extraction ────────────────────────────────────
|
|
10
10
|
/** When set, calls whose function/method name matches this key are replaced with havoc. */
|
|
@@ -87,11 +87,16 @@ function detectCrossFileExtern(callee, sourceFile) {
|
|
|
87
87
|
// types in the callee's own type-parameter namespace, so these names match
|
|
88
88
|
// what `params`/`returnType` reference — declare them on the emitted axiom.
|
|
89
89
|
const typeParams = sig.getTypeParameters().map(tp => tp.getText());
|
|
90
|
+
// Print types relative to the call site (enclosingNode = callee, alias names
|
|
91
|
+
// kept): a bare `TMsg`, not `import("/abs/path/transcript").TMsg` — the
|
|
92
|
+
// importing module declares the datatype locally, so the axiom must use the
|
|
93
|
+
// local name.
|
|
94
|
+
const externTypeText = (t) => t.getText(callee, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
|
|
90
95
|
const params = sig.getParameters().map(p => ({
|
|
91
96
|
name: p.getName(),
|
|
92
|
-
tsType: p.getTypeAtLocation(callee)
|
|
97
|
+
tsType: externTypeText(p.getTypeAtLocation(callee)),
|
|
93
98
|
}));
|
|
94
|
-
const returnType = sig.getReturnType()
|
|
99
|
+
const returnType = externTypeText(sig.getReturnType());
|
|
95
100
|
let qualified;
|
|
96
101
|
if (Node.isPropertyAccessExpression(callee)) {
|
|
97
102
|
qualified = `${callee.getExpression().getText()}.${callee.getName()}`;
|
|
@@ -309,9 +314,10 @@ function extractExpr(node) {
|
|
|
309
314
|
// Template literal: `foo${x}bar` → "foo" + x + "bar"
|
|
310
315
|
if (Node.isTemplateExpression(node)) {
|
|
311
316
|
const parts = [];
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
317
|
+
// Always push the head, even when empty: a leading string literal anchors the
|
|
318
|
+
// whole chain as string-typed so each interpolated value is stringified (not
|
|
319
|
+
// added numerically — `${a}${b}` is concatenation, not `a + b`).
|
|
320
|
+
parts.push({ kind: "str", value: node.getHead().getLiteralText() });
|
|
315
321
|
for (const span of node.getTemplateSpans()) {
|
|
316
322
|
parts.push(extractExpr(span.getExpression()));
|
|
317
323
|
const text = span.getLiteral().getLiteralText();
|
|
@@ -481,43 +487,42 @@ function extractExpr(node) {
|
|
|
481
487
|
}
|
|
482
488
|
// Object literal: { res: true, done: false } or { ...obj, res: true }
|
|
483
489
|
if (Node.isObjectLiteralExpression(node)) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
490
|
+
// Fold properties in source order: a later spread overrides everything before
|
|
491
|
+
// it, a named field is a record-update on the accumulator, a computed key is a
|
|
492
|
+
// map `.set`. Order matters: if `a` has `k`, `{ k: v, ...a }` is `a`, not `a.(k := v)`.
|
|
493
|
+
let acc = null;
|
|
494
|
+
const update = (name, value) => {
|
|
495
|
+
acc = acc && acc.kind === "record"
|
|
496
|
+
? { kind: "record", spread: acc.spread, fields: [...acc.fields, { name, value }] }
|
|
497
|
+
: { kind: "record", spread: acc, fields: [{ name, value }] };
|
|
498
|
+
};
|
|
487
499
|
for (const prop of node.getProperties()) {
|
|
488
500
|
if (Node.isSpreadAssignment(prop)) {
|
|
489
|
-
|
|
501
|
+
acc = extractExpr(prop.getExpression());
|
|
490
502
|
}
|
|
491
503
|
else if (Node.isShorthandPropertyAssignment(prop)) {
|
|
492
504
|
const name = prop.getName();
|
|
493
|
-
|
|
505
|
+
update(name, { kind: "var", name });
|
|
494
506
|
}
|
|
495
507
|
else if (Node.isPropertyAssignment(prop)) {
|
|
496
508
|
const nameNode = prop.getNameNode();
|
|
497
509
|
const init = prop.getInitializer();
|
|
498
|
-
if (init
|
|
499
|
-
|
|
510
|
+
if (!init)
|
|
511
|
+
continue;
|
|
512
|
+
if (Node.isComputedPropertyName(nameNode)) {
|
|
513
|
+
// { ...base, [k]: v } → base.set(k, v); { [k]: v } → {}.set(k, v)
|
|
514
|
+
const base = acc ?? { kind: "record", spread: null, fields: [] };
|
|
515
|
+
acc = { kind: "call", fn: { kind: "field", obj: base, field: "set" }, args: [extractExpr(nameNode.getExpression()), extractExpr(init)] };
|
|
500
516
|
}
|
|
501
|
-
else
|
|
517
|
+
else {
|
|
502
518
|
// String-literal keys (e.g. `"bun run": 3`) — use the unquoted literal
|
|
503
519
|
// value; otherwise `prop.getName()` may include surrounding quotes.
|
|
504
520
|
const name = Node.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : prop.getName();
|
|
505
|
-
|
|
521
|
+
update(name, extractExpr(init));
|
|
506
522
|
}
|
|
507
523
|
}
|
|
508
524
|
}
|
|
509
|
-
|
|
510
|
-
// No spread: { [k]: v } → {}.set(k, v) (empty map base)
|
|
511
|
-
if (computedFields.length > 0) {
|
|
512
|
-
let result = spread ?? { kind: "record", spread: null, fields: [] };
|
|
513
|
-
for (const cf of computedFields) {
|
|
514
|
-
result = { kind: "call",
|
|
515
|
-
fn: { kind: "field", obj: result, field: "set" },
|
|
516
|
-
args: [cf.key, cf.value] };
|
|
517
|
-
}
|
|
518
|
-
return result;
|
|
519
|
-
}
|
|
520
|
-
return { kind: "record", spread, fields };
|
|
525
|
+
return acc ?? { kind: "record", spread: null, fields: [] };
|
|
521
526
|
}
|
|
522
527
|
// Ternary: cond ? then : else
|
|
523
528
|
if (Node.isConditionalExpression(node)) {
|
|
@@ -555,8 +560,12 @@ function extractExpr(node) {
|
|
|
555
560
|
if (Node.isArrayLiteralExpression(arg)) {
|
|
556
561
|
return { kind: "emptyCollection", collectionType: "Set", tsType, initElems: arg.getElements().map(e => extractExpr(e)) };
|
|
557
562
|
}
|
|
558
|
-
|
|
559
|
-
|
|
563
|
+
const argSymbol = arg.getType().getSymbol()?.getName() ?? arg.getType().getAliasSymbol()?.getName();
|
|
564
|
+
// new Set(existingSet) — identity (sets are value types)
|
|
565
|
+
if (argSymbol === "Set")
|
|
566
|
+
return extractExpr(arg);
|
|
567
|
+
// new Set(arr) — build a deduplicated set from the array's elements
|
|
568
|
+
return { kind: "call", fn: { kind: "var", name: "__setFromArray" }, args: [extractExpr(arg)] };
|
|
560
569
|
}
|
|
561
570
|
return { kind: "emptyCollection", collectionType: name, tsType };
|
|
562
571
|
}
|
|
@@ -1557,6 +1566,7 @@ function extractStmts(stmts) {
|
|
|
1557
1566
|
// inside a `{ }` case block is stripped, while a `break` inside a
|
|
1558
1567
|
// nested loop stays put.
|
|
1559
1568
|
const stripExitBreaks = (b) => b.filter(st => st.kind !== "break");
|
|
1569
|
+
const isExit = (st) => !!st && ["break", "return", "throw", "continue"].includes(st.kind);
|
|
1560
1570
|
let fallthrough = [];
|
|
1561
1571
|
for (const clause of s.getClauses()) {
|
|
1562
1572
|
if (Node.isCaseClause(clause)) {
|
|
@@ -1565,7 +1575,10 @@ function extractStmts(stmts) {
|
|
|
1565
1575
|
fallthrough.push(label);
|
|
1566
1576
|
continue;
|
|
1567
1577
|
}
|
|
1568
|
-
const
|
|
1578
|
+
const raw = extractStmts(clause.getStatements());
|
|
1579
|
+
if (!isExit(raw[raw.length - 1]))
|
|
1580
|
+
throw new Error(`switch case "${label}" at line ${line}: a non-empty case must end with break/return/throw; fall-through into the next case is not supported`);
|
|
1581
|
+
const body = stripExitBreaks(raw);
|
|
1569
1582
|
for (const l of fallthrough)
|
|
1570
1583
|
cases.push({ label: l, body });
|
|
1571
1584
|
cases.push({ label, body });
|
package/tools/dist/lean-emit.js
CHANGED
|
@@ -77,6 +77,7 @@ function escapeName(name) {
|
|
|
77
77
|
}
|
|
78
78
|
// ── Operator precedence (for parenthesization) ──────────────
|
|
79
79
|
const PREC = {
|
|
80
|
+
"↔": 0, // Lean: Iff (20) binds looser than → (25)
|
|
80
81
|
"→": 1, "∨": 2, "∧": 3,
|
|
81
82
|
"=": 4, "≠": 4, "≥": 4, "≤": 4, ">": 4, "<": 4,
|
|
82
83
|
"+": 5, "-": 5, "++": 5, "arrayConcat": 5, "*": 6, "/": 6, "%": 6,
|
|
@@ -96,15 +97,15 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
96
97
|
if (method === "some")
|
|
97
98
|
return `${obj}.${monadic ? "anyM" : "any"} ${args[0]}`;
|
|
98
99
|
if (method === "includes")
|
|
99
|
-
return `${obj}.contains ${args[0]}`;
|
|
100
|
+
return args.length > 1 ? `(${obj}.extract ${args[1]} ${obj}.size).contains ${args[0]}` : `${obj}.contains ${args[0]}`;
|
|
100
101
|
if (method === "find")
|
|
101
102
|
return `${obj}.find? ${args[0]}`;
|
|
102
103
|
if (method === "with")
|
|
103
104
|
return `${obj}.set! ${args[0]} ${args[1]}`;
|
|
104
105
|
if (method === "push")
|
|
105
|
-
return `Array.push ${obj} ${args[0]}`;
|
|
106
|
+
return args.length === 1 ? `Array.push ${obj} ${args[0]}` : `${obj} ++ #[${args.join(", ")}]`;
|
|
106
107
|
if (method === "concat")
|
|
107
|
-
return `Array.push ${obj} ${args[0]}`;
|
|
108
|
+
return args.length === 1 ? `Array.push ${obj} ${args[0]}` : `${obj} ++ #[${args.join(", ")}]`;
|
|
108
109
|
// arr.slice → Array.extract. No-arg slice is a full copy (Array is a value
|
|
109
110
|
// type in Lean, so the receiver itself); one arg drops the prefix, two args
|
|
110
111
|
// give the half-open range. Matches JS for non-negative bounds (negative
|
|
@@ -184,7 +185,7 @@ function emitExpr(e, parentPrec) {
|
|
|
184
185
|
case "emptySet": return `Std.HashSet.empty`;
|
|
185
186
|
case "methodCall": {
|
|
186
187
|
const obj = emitExpr(e.obj);
|
|
187
|
-
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "methodCall";
|
|
188
|
+
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "methodCall" || e.obj.kind === "if" || e.obj.kind === "let";
|
|
188
189
|
const receiver = wrap ? `(${obj})` : obj;
|
|
189
190
|
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));
|
|
190
191
|
return emitMethodCall(e.objTy.kind, e.method, e.monadic, receiver, args);
|
|
@@ -213,11 +214,23 @@ function emitExpr(e, parentPrec) {
|
|
|
213
214
|
return `${wrap ? `(${recv})` : recv}.contains ${emitExpr(e.left)}`;
|
|
214
215
|
}
|
|
215
216
|
const op = e.op === "arrayConcat" ? "++" : e.op;
|
|
216
|
-
|
|
217
|
+
// ↔ does not chain in Lean — a nested iff operand needs parens.
|
|
218
|
+
const childPrec = e.op === "↔" ? prec(e.op) + 1 : prec(e.op);
|
|
219
|
+
// `-`, `/`, `%` are left-associative and non-associative, so an equal-
|
|
220
|
+
// precedence right operand must be parenthesized: `a - (b - c)` would
|
|
221
|
+
// otherwise emit as `a - b - c`, i.e. `(a - b) - c`.
|
|
222
|
+
const rightPrec = e.op === "↔" ? childPrec
|
|
223
|
+
: ["-", "/", "%"].includes(e.op) ? prec(e.op) + 1 : childPrec;
|
|
224
|
+
const s = `${wrapOperand(e.left, childPrec)} ${op} ${wrapOperand(e.right, rightPrec)}`;
|
|
217
225
|
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
218
226
|
}
|
|
219
227
|
case "implies": {
|
|
220
|
-
|
|
228
|
+
// Premises bind at →'s level: a nested-implication premise must keep its
|
|
229
|
+
// parens (→ is right-associative, so `(a → b) → c` ≠ `a → b → c`), and
|
|
230
|
+
// ↔ binds looser than → in Lean. The conclusion is the right-assoc tail,
|
|
231
|
+
// where a nested implication is safe bare — only ↔ needs parens there.
|
|
232
|
+
const wrapIff = (x) => x.kind === "binop" && x.op === "↔" ? `(${emitExpr(x)})` : undefined;
|
|
233
|
+
const parts = [...e.premises.map(p => wrapOperand(p, prec("→"))), wrapIff(e.conclusion) ?? emitExpr(e.conclusion)];
|
|
221
234
|
const s = parts.join(" → ");
|
|
222
235
|
return parentPrec !== undefined ? `(${s})` : s;
|
|
223
236
|
}
|
|
@@ -231,6 +244,16 @@ function emitExpr(e, parentPrec) {
|
|
|
231
244
|
// SetToSeq → .toArray for Lean (HashSet has native toArray)
|
|
232
245
|
if (e.fn === "SetToSeq" && args.length === 1)
|
|
233
246
|
return `${args[0]}.toArray`;
|
|
247
|
+
if (e.fn === "SetFromSeq" && args.length === 1)
|
|
248
|
+
return `Std.HashSet.ofList ${args[0]}.toList`;
|
|
249
|
+
if (e.fn === "ToString" && args.length === 1)
|
|
250
|
+
return `toString ${args[0]}`;
|
|
251
|
+
// JSRem (JS truncated remainder) → Lean's native truncated `Int.tmod`
|
|
252
|
+
if (e.fn === "JSRem" && args.length === 2)
|
|
253
|
+
return `Int.tmod ${args[0]} ${args[1]}`;
|
|
254
|
+
// JSTruncDiv (JS truncated bigint division) → Lean's native `Int.tdiv`
|
|
255
|
+
if (e.fn === "JSTruncDiv" && args.length === 2)
|
|
256
|
+
return `Int.tdiv ${args[0]} ${args[1]}`;
|
|
234
257
|
// perm(a, b) → `List.Perm` on the underlying lists. Dafny lowers it to
|
|
235
258
|
// `multiset(a) == multiset(b)`; the Lean image is `a.toList ~ b.toList`,
|
|
236
259
|
// which mathlib's `List.Perm` provides (reflexivity, symmetry,
|
package/tools/dist/narrow.js
CHANGED
|
@@ -249,6 +249,8 @@ function ruleEarlyReturnConsume(s, rest) {
|
|
|
249
249
|
const noneBranch = check.negated ? s.then : s.else;
|
|
250
250
|
if (someBranch.length !== 0)
|
|
251
251
|
return null;
|
|
252
|
+
if (!isTerminating(noneBranch))
|
|
253
|
+
return null;
|
|
252
254
|
return {
|
|
253
255
|
kind: "someMatch",
|
|
254
256
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
@@ -292,11 +294,14 @@ function ruleEarlyReturnOrChain(s, rest) {
|
|
|
292
294
|
let inner = rest;
|
|
293
295
|
for (let i = checks.length - 1; i >= 0; i--) {
|
|
294
296
|
const check = checks[i];
|
|
297
|
+
const someBody = canBeFalsy(check)
|
|
298
|
+
? [{ kind: "if", cond: bound(check), then: inner, else: s.then }]
|
|
299
|
+
: inner;
|
|
295
300
|
inner = [{
|
|
296
301
|
kind: "someMatch",
|
|
297
302
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
298
303
|
binder: check.binderHint,
|
|
299
|
-
someBody
|
|
304
|
+
someBody,
|
|
300
305
|
noneBody: s.then,
|
|
301
306
|
}];
|
|
302
307
|
}
|
|
@@ -582,11 +587,14 @@ function ruleIfAndOptional(s) {
|
|
|
582
587
|
return null;
|
|
583
588
|
const { check, restCond } = extracted;
|
|
584
589
|
const innerIf = { kind: "if", cond: restCond, then: s.then, else: [] };
|
|
590
|
+
const someBody = canBeFalsy(check)
|
|
591
|
+
? [{ kind: "if", cond: bound(check), then: [walkStmt(innerIf)], else: [] }]
|
|
592
|
+
: [walkStmt(innerIf)];
|
|
585
593
|
return {
|
|
586
594
|
kind: "someMatch",
|
|
587
595
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
588
596
|
binder: check.binderHint,
|
|
589
|
-
someBody
|
|
597
|
+
someBody,
|
|
590
598
|
noneBody: [],
|
|
591
599
|
};
|
|
592
600
|
}
|
|
@@ -776,8 +784,16 @@ function ruleDiscriminantChain(stmts) {
|
|
|
776
784
|
}
|
|
777
785
|
if (cases.length === 0)
|
|
778
786
|
return null;
|
|
787
|
+
// If every case terminates, the trailing statements are the default arm
|
|
788
|
+
// (preserving the clean dispatch-as-expression shape). Otherwise the tail runs
|
|
789
|
+
// after the match for every variant, so leave it to the caller (empty default)
|
|
790
|
+
// rather than mis-routing it into the default arm only.
|
|
791
|
+
if (cases.every(c => isTerminating(c.body))) {
|
|
792
|
+
return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
|
|
793
|
+
cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
|
|
794
|
+
}
|
|
779
795
|
return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
|
|
780
|
-
cases, fallthrough:
|
|
796
|
+
cases, fallthrough: [] }, consumed };
|
|
781
797
|
}
|
|
782
798
|
/** Rule (list-level): `if (x.kind !== "v") terminate; rest` → tagMatch
|
|
783
799
|
* with cases = [{ variant: v, body: rest }] and fallthrough = terminate. */
|
|
@@ -809,12 +825,16 @@ function ruleLetCondAndOptional(s) {
|
|
|
809
825
|
if (!extracted)
|
|
810
826
|
return null;
|
|
811
827
|
const { check, restCond } = extracted;
|
|
828
|
+
const assignIf = { kind: "if", cond: restCond,
|
|
829
|
+
then: [{ kind: "assign", target: s.name, value: s.init.then }], else: [] };
|
|
830
|
+
const someBody = canBeFalsy(check)
|
|
831
|
+
? [{ kind: "if", cond: bound(check), then: [assignIf], else: [] }]
|
|
832
|
+
: [assignIf];
|
|
812
833
|
const sm = {
|
|
813
834
|
kind: "someMatch",
|
|
814
835
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
815
836
|
binder: check.binderHint,
|
|
816
|
-
someBody
|
|
817
|
-
then: [{ kind: "assign", target: s.name, value: s.init.then }], else: [] }],
|
|
837
|
+
someBody,
|
|
818
838
|
noneBody: [],
|
|
819
839
|
};
|
|
820
840
|
return [
|
|
@@ -889,11 +909,15 @@ function ruleConditionalAndOptional(e) {
|
|
|
889
909
|
kind: "conditional",
|
|
890
910
|
cond: restCond, then: e.then, else: e.else, ty: e.ty,
|
|
891
911
|
};
|
|
912
|
+
const someExpr = walkExpr(innerCond);
|
|
913
|
+
const someBody = canBeFalsy(check)
|
|
914
|
+
? { kind: "conditional", cond: bound(check), then: someExpr, else: e.else, ty: e.ty }
|
|
915
|
+
: someExpr;
|
|
892
916
|
return {
|
|
893
917
|
kind: "someMatch",
|
|
894
918
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
895
919
|
binder: check.binderHint,
|
|
896
|
-
someBody
|
|
920
|
+
someBody, noneBody: e.else, ty: e.ty,
|
|
897
921
|
};
|
|
898
922
|
}
|
|
899
923
|
/** Rule (statement): `if (<rest> && Array.isArray(path) && <more>) then [else]`
|
package/tools/dist/resolve.js
CHANGED
|
@@ -685,7 +685,9 @@ function resolveExpr(e, ctx) {
|
|
|
685
685
|
}
|
|
686
686
|
}
|
|
687
687
|
let ty = { kind: "unknown" };
|
|
688
|
-
|
|
688
|
+
// <==> is bool like the comparisons; unlike ==>, neither side narrows
|
|
689
|
+
// the other (no premise to assume).
|
|
690
|
+
if (["===", "!==", ">=", "<=", ">", "<", "in", "<==>"].includes(e.op))
|
|
689
691
|
ty = { kind: "bool" };
|
|
690
692
|
else if (e.op === "&&")
|
|
691
693
|
ty = right.ty;
|
|
@@ -727,6 +729,16 @@ function resolveExpr(e, ctx) {
|
|
|
727
729
|
const fn = { kind: "var", name: "Perm", ty: { kind: "unknown" } };
|
|
728
730
|
return { kind: "call", fn, args: [a, b], ty: { kind: "bool" }, callKind: "pure" };
|
|
729
731
|
}
|
|
732
|
+
// new Set(arr): build a deduplicated set from the array's elements (extract
|
|
733
|
+
// marks the array form `__setFromArray`). Lowers to the SetFromSeq preamble
|
|
734
|
+
// (Dafny `set x | x in s`); size/membership are then set semantics.
|
|
735
|
+
if (e.fn.kind === "var" && e.fn.name === "__setFromArray" && e.args.length === 1) {
|
|
736
|
+
const arr = resolveExpr(e.args[0], ctx);
|
|
737
|
+
if (arr.ty.kind !== "array")
|
|
738
|
+
throw new Error(`new Set(...) expects an array argument (got ${arr.ty.kind})`);
|
|
739
|
+
const fn = { kind: "var", name: "SetFromSeq", ty: { kind: "unknown" } };
|
|
740
|
+
return { kind: "call", fn, args: [arr], ty: { kind: "set", elem: arr.ty.elem }, callKind: "pure" };
|
|
741
|
+
}
|
|
730
742
|
// Extern dispatch: `NS.method(args)` where NS.method is declared via
|
|
731
743
|
// `//@ extern`. Rewrite into a flat-name call (`NS_method(args)`) so the
|
|
732
744
|
// rest of the pipeline sees an ordinary pure function. The extern's
|
package/tools/dist/specparser.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Spec expression parser.
|
|
3
3
|
* Parses //@ annotation expressions into RawExpr AST nodes.
|
|
4
4
|
*/
|
|
5
|
-
const MULTI_OPS = ["==>", "===", "!==", "==", "!=", ">=", "<=", "&&", "||"];
|
|
5
|
+
const MULTI_OPS = ["<==>", "==>", "===", "!==", "==", "!=", ">=", "<=", "&&", "||"];
|
|
6
6
|
function tokenize(input) {
|
|
7
7
|
const tokens = [];
|
|
8
8
|
let i = 0;
|
|
@@ -20,8 +20,22 @@ function tokenize(input) {
|
|
|
20
20
|
const quote = input[i];
|
|
21
21
|
i++;
|
|
22
22
|
let s = "";
|
|
23
|
-
while (i < input.length && input[i] !== quote)
|
|
24
|
-
|
|
23
|
+
while (i < input.length && input[i] !== quote) {
|
|
24
|
+
if (input[i] === "\\") {
|
|
25
|
+
// Standard escapes, where TS source, Dafny, and Lean all agree.
|
|
26
|
+
// The emitters re-escape on output, so the round trip is faithful.
|
|
27
|
+
const esc = input[i + 1];
|
|
28
|
+
const mapped = esc === "n" ? "\n" : esc === "r" ? "\r" : esc === "t" ? "\t"
|
|
29
|
+
: esc === "0" ? "\0" : esc === "\\" || esc === '"' || esc === "'" ? esc : null;
|
|
30
|
+
if (mapped === null)
|
|
31
|
+
throw new Error(`Unsupported string escape '\\${esc}' at ${i} in: ${input}`);
|
|
32
|
+
s += mapped;
|
|
33
|
+
i += 2;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
s += input[i++];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
25
39
|
if (i < input.length)
|
|
26
40
|
i++;
|
|
27
41
|
tokens.push({ type: "str", value: s });
|
|
@@ -103,11 +117,19 @@ class Parser {
|
|
|
103
117
|
return false;
|
|
104
118
|
}
|
|
105
119
|
parse() {
|
|
106
|
-
const r = this.
|
|
120
|
+
const r = this.parseIff();
|
|
107
121
|
if (this.pos < this.tokens.length)
|
|
108
122
|
throw new Error(`Unexpected: ${JSON.stringify(this.peek())}`);
|
|
109
123
|
return r;
|
|
110
124
|
}
|
|
125
|
+
// <==> binds loosest (Dafny precedence: a ==> b <==> c is (a ==> b) <==> c),
|
|
126
|
+
// right-associative like ==> — immaterial semantically, iff is associative.
|
|
127
|
+
parseIff() {
|
|
128
|
+
const left = this.parseImplies();
|
|
129
|
+
if (this.match("op", "<==>"))
|
|
130
|
+
return { kind: "binop", op: "<==>", left, right: this.parseIff() };
|
|
131
|
+
return left;
|
|
132
|
+
}
|
|
111
133
|
parseImplies() {
|
|
112
134
|
const left = this.parseTernary();
|
|
113
135
|
if (this.match("op", "==>"))
|
|
@@ -117,9 +139,9 @@ class Parser {
|
|
|
117
139
|
parseTernary() {
|
|
118
140
|
const cond = this.parseOr();
|
|
119
141
|
if (this.match("op", "?")) {
|
|
120
|
-
const then_ = this.
|
|
142
|
+
const then_ = this.parseIff();
|
|
121
143
|
this.expect("punc", ":");
|
|
122
|
-
const else_ = this.
|
|
144
|
+
const else_ = this.parseIff();
|
|
123
145
|
return { kind: "conditional", cond, then: then_, else: else_ };
|
|
124
146
|
}
|
|
125
147
|
return cond;
|
|
@@ -187,16 +209,16 @@ class Parser {
|
|
|
187
209
|
expr = { kind: "field", obj: expr, field: this.expect("ident").value };
|
|
188
210
|
}
|
|
189
211
|
else if (this.match("punc", "[")) {
|
|
190
|
-
const idx = this.
|
|
212
|
+
const idx = this.parseIff();
|
|
191
213
|
this.expect("punc", "]");
|
|
192
214
|
expr = { kind: "index", obj: expr, idx };
|
|
193
215
|
}
|
|
194
216
|
else if (this.match("punc", "(")) {
|
|
195
217
|
const args = [];
|
|
196
218
|
if (!this.match("punc", ")")) {
|
|
197
|
-
args.push(this.
|
|
219
|
+
args.push(this.parseIff());
|
|
198
220
|
while (this.match("punc", ","))
|
|
199
|
-
args.push(this.
|
|
221
|
+
args.push(this.parseIff());
|
|
200
222
|
this.expect("punc", ")");
|
|
201
223
|
}
|
|
202
224
|
expr = { kind: "call", fn: expr, args };
|
|
@@ -276,7 +298,7 @@ class Parser {
|
|
|
276
298
|
varType = ty;
|
|
277
299
|
}
|
|
278
300
|
this.expect("punc", ",");
|
|
279
|
-
const body = this.
|
|
301
|
+
const body = this.parseIff();
|
|
280
302
|
this.expect("punc", ")");
|
|
281
303
|
return { kind: q, var: v, varType, body };
|
|
282
304
|
}
|
|
@@ -285,7 +307,7 @@ class Parser {
|
|
|
285
307
|
}
|
|
286
308
|
if (t.type === "punc" && t.value === "(") {
|
|
287
309
|
this.advance();
|
|
288
|
-
const expr = this.
|
|
310
|
+
const expr = this.parseIff();
|
|
289
311
|
this.expect("punc", ")");
|
|
290
312
|
return expr;
|
|
291
313
|
}
|
|
@@ -293,9 +315,9 @@ class Parser {
|
|
|
293
315
|
this.advance();
|
|
294
316
|
const elems = [];
|
|
295
317
|
if (!this.match("punc", "]")) {
|
|
296
|
-
elems.push(this.
|
|
318
|
+
elems.push(this.parseIff());
|
|
297
319
|
while (this.match("punc", ","))
|
|
298
|
-
elems.push(this.
|
|
320
|
+
elems.push(this.parseIff());
|
|
299
321
|
this.expect("punc", "]");
|
|
300
322
|
}
|
|
301
323
|
return { kind: "arrayLiteral", elems };
|
|
@@ -306,11 +328,11 @@ class Parser {
|
|
|
306
328
|
if (!this.match("punc", "}")) {
|
|
307
329
|
const name = this.expect("ident").value;
|
|
308
330
|
this.expect("punc", ":");
|
|
309
|
-
fields.push({ name, value: this.
|
|
331
|
+
fields.push({ name, value: this.parseIff() });
|
|
310
332
|
while (this.match("punc", ",")) {
|
|
311
333
|
const n = this.expect("ident").value;
|
|
312
334
|
this.expect("punc", ":");
|
|
313
|
-
fields.push({ name: n, value: this.
|
|
335
|
+
fields.push({ name: n, value: this.parseIff() });
|
|
314
336
|
}
|
|
315
337
|
this.expect("punc", "}");
|
|
316
338
|
}
|
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
|
-
}
|