lemmascript 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -31
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +47 -53
- package/tools/dist/dafny-emit.js +495 -93
- package/tools/dist/extract.js +847 -46
- package/tools/dist/ir.js +2 -2
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +397 -0
- package/tools/dist/lsc.js +62 -44
- package/tools/dist/resolve.js +500 -34
- package/tools/dist/specparser.js +66 -9
- package/tools/dist/transform.js +813 -202
- package/tools/dist/types.js +59 -13
package/tools/dist/transform.js
CHANGED
|
@@ -1,70 +1,142 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transform — Typed IR →
|
|
2
|
+
* Transform — Typed IR → Backend IR.
|
|
3
3
|
*
|
|
4
4
|
* Consumes resolved types and classifications.
|
|
5
5
|
* No type lookups, no string parsing, no re-inference.
|
|
6
6
|
*/
|
|
7
|
-
import { parseTsType
|
|
7
|
+
import { parseTsType } from "./types.js";
|
|
8
|
+
// ── Generic IR walkers ──────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Map over all sub-expressions in an Expr. `f` is called on each node;
|
|
11
|
+
* if it returns non-null, that replaces the node (and recursion stops).
|
|
12
|
+
* If it returns null, the walker recurses into children.
|
|
13
|
+
*/
|
|
14
|
+
function mapExpr(e, f) {
|
|
15
|
+
const hit = f(e);
|
|
16
|
+
if (hit)
|
|
17
|
+
return hit;
|
|
18
|
+
const r = (x) => mapExpr(x, f);
|
|
19
|
+
switch (e.kind) {
|
|
20
|
+
case "var":
|
|
21
|
+
case "num":
|
|
22
|
+
case "bool":
|
|
23
|
+
case "str":
|
|
24
|
+
case "constructor":
|
|
25
|
+
case "emptyMap":
|
|
26
|
+
case "emptySet":
|
|
27
|
+
case "havoc": return e;
|
|
28
|
+
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
29
|
+
case "unop": return { ...e, expr: r(e.expr) };
|
|
30
|
+
case "implies": return { ...e, premises: e.premises.map(r), conclusion: r(e.conclusion) };
|
|
31
|
+
case "app": return { ...e, args: e.args.map(r) };
|
|
32
|
+
case "field": return { ...e, obj: r(e.obj) };
|
|
33
|
+
case "toNat": return { ...e, expr: r(e.expr) };
|
|
34
|
+
case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
|
|
35
|
+
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
36
|
+
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
37
|
+
case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
|
|
38
|
+
case "match": {
|
|
39
|
+
const scr = typeof e.scrutinee === "string" ? e.scrutinee : r(e.scrutinee);
|
|
40
|
+
return { ...e, scrutinee: scr, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
|
|
41
|
+
}
|
|
42
|
+
case "forall": return { ...e, body: r(e.body) };
|
|
43
|
+
case "exists": return { ...e, body: r(e.body) };
|
|
44
|
+
case "let": return { ...e, value: r(e.value), body: r(e.body) };
|
|
45
|
+
case "methodCall": return { ...e, obj: r(e.obj), args: e.args.map(r) };
|
|
46
|
+
case "lambda": return e;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Map over all expressions in a statement tree. */
|
|
50
|
+
function mapStmt(s, f) {
|
|
51
|
+
const r = (e) => mapExpr(e, f);
|
|
52
|
+
switch (s.kind) {
|
|
53
|
+
case "let": return { ...s, value: r(s.value) };
|
|
54
|
+
case "assign": return { ...s, value: r(s.value) };
|
|
55
|
+
case "bind": return { ...s, value: r(s.value) };
|
|
56
|
+
case "let-bind": return { ...s, value: r(s.value) };
|
|
57
|
+
case "return": return { ...s, value: r(s.value) };
|
|
58
|
+
case "break":
|
|
59
|
+
case "continue": return s;
|
|
60
|
+
case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapStmt(t, f)), else: s.else.map(t => mapStmt(t, f)) };
|
|
61
|
+
case "match": return { ...s, arms: s.arms.map(a => ({ ...a, body: a.body.map(t => mapStmt(t, f)) })) };
|
|
62
|
+
case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
|
|
63
|
+
case "forin": return { ...s, bound: r(s.bound), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
|
|
64
|
+
case "ghostLet": return { ...s, value: r(s.value) };
|
|
65
|
+
case "ghostAssign": return { ...s, value: r(s.value) };
|
|
66
|
+
case "assert": return { ...s, expr: r(s.expr) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function mapStmts(stmts, f) {
|
|
70
|
+
return stmts.map(s => mapStmt(s, f));
|
|
71
|
+
}
|
|
72
|
+
/** Map over all sub-expressions in a TExpr (typed IR). */
|
|
73
|
+
function mapTExpr(e, f) {
|
|
74
|
+
const hit = f(e);
|
|
75
|
+
if (hit)
|
|
76
|
+
return hit;
|
|
77
|
+
const r = (x) => mapTExpr(x, f);
|
|
78
|
+
switch (e.kind) {
|
|
79
|
+
case "var":
|
|
80
|
+
case "num":
|
|
81
|
+
case "str":
|
|
82
|
+
case "bool":
|
|
83
|
+
case "result":
|
|
84
|
+
case "havoc": return e;
|
|
85
|
+
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
86
|
+
case "unop": return { ...e, expr: r(e.expr) };
|
|
87
|
+
case "call": return { ...e, fn: r(e.fn), args: e.args.map(r) };
|
|
88
|
+
case "index": return { ...e, obj: r(e.obj), idx: r(e.idx) };
|
|
89
|
+
case "field": return { ...e, obj: r(e.obj) };
|
|
90
|
+
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
91
|
+
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
92
|
+
case "conditional": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
|
|
93
|
+
case "forall": return { ...e, body: r(e.body) };
|
|
94
|
+
case "exists": return { ...e, body: r(e.body) };
|
|
95
|
+
case "lambda": return e;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Map over all expressions in a TStmt tree (typed IR). */
|
|
99
|
+
function mapTStmt(s, f) {
|
|
100
|
+
const r = (e) => mapTExpr(e, f);
|
|
101
|
+
switch (s.kind) {
|
|
102
|
+
case "let": return { ...s, init: r(s.init) };
|
|
103
|
+
case "assign": return { ...s, value: r(s.value) };
|
|
104
|
+
case "return": return { ...s, value: r(s.value) };
|
|
105
|
+
case "break":
|
|
106
|
+
case "continue":
|
|
107
|
+
case "throw": return s;
|
|
108
|
+
case "expr": return { ...s, expr: r(s.expr) };
|
|
109
|
+
case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapTStmt(t, f)), else: s.else.map(t => mapTStmt(t, f)) };
|
|
110
|
+
case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapTStmt(t, f)) };
|
|
111
|
+
case "switch": return { ...s, expr: r(s.expr), cases: s.cases.map(c => ({ ...c, body: c.body.map(t => mapTStmt(t, f)) })), defaultBody: s.defaultBody.map(t => mapTStmt(t, f)) };
|
|
112
|
+
case "forof": return { ...s, iterable: r(s.iterable), invariants: s.invariants.map(r), body: s.body.map(t => mapTStmt(t, f)) };
|
|
113
|
+
case "ghostLet": return { ...s, init: r(s.init) };
|
|
114
|
+
case "ghostAssign": return { ...s, value: r(s.value) };
|
|
115
|
+
case "assert": return { ...s, expr: r(s.expr) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
8
118
|
export const LEAN_OPTIONS = {
|
|
9
119
|
backend: "lean",
|
|
10
120
|
monadic: true,
|
|
11
|
-
dotMethods: {
|
|
12
|
-
array: {
|
|
13
|
-
map: { pure: "map", monadic: "mapM" },
|
|
14
|
-
filter: { pure: "filter", monadic: "filterM" },
|
|
15
|
-
every: { pure: "all", monadic: "allM" },
|
|
16
|
-
some: { pure: "any", monadic: "anyM" },
|
|
17
|
-
includes: { pure: "contains" },
|
|
18
|
-
find: { pure: "find?" },
|
|
19
|
-
with: { pure: "set!" },
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
methodTable: {
|
|
23
|
-
string: {
|
|
24
|
-
indexOf: "JSString.indexOf",
|
|
25
|
-
slice: "JSString.slice",
|
|
26
|
-
},
|
|
27
|
-
array: {
|
|
28
|
-
push: "Array.push",
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
121
|
};
|
|
32
122
|
export const DAFNY_OPTIONS = {
|
|
33
123
|
backend: "dafny",
|
|
34
124
|
monadic: false,
|
|
35
|
-
dotMethods: {
|
|
36
|
-
array: {
|
|
37
|
-
map: { pure: "map" },
|
|
38
|
-
filter: { pure: "filter" },
|
|
39
|
-
every: { pure: "every" },
|
|
40
|
-
some: { pure: "some" },
|
|
41
|
-
includes: { pure: "includes" },
|
|
42
|
-
with: { pure: "with" },
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
methodTable: {
|
|
46
|
-
string: {
|
|
47
|
-
indexOf: "StringIndexOf",
|
|
48
|
-
slice: "StringSlice",
|
|
49
|
-
},
|
|
50
|
-
array: {
|
|
51
|
-
push: "SeqPush",
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
125
|
};
|
|
55
126
|
/** Active options — set before each transform call. */
|
|
56
|
-
let _opts =
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
127
|
+
let _opts = DAFNY_OPTIONS;
|
|
128
|
+
/** Type declarations — set once per module transform for discriminated union handling. */
|
|
129
|
+
let _typeDecls = [];
|
|
130
|
+
/** Prefix match-bound field names to avoid capturing user variables.
|
|
131
|
+
* When prefix is given (the scrutinee name), include it to avoid
|
|
132
|
+
* collisions in nested matches on different variables. */
|
|
133
|
+
function matchBinder(fieldName, prefix) {
|
|
134
|
+
return prefix ? `_${prefix}_${fieldName}` : `_${fieldName}`;
|
|
60
135
|
}
|
|
136
|
+
const _forofCounters = new Map();
|
|
61
137
|
function isNat(ty) { return ty.kind === "nat"; }
|
|
62
138
|
function isArray(ty) { return ty.kind === "array"; }
|
|
63
139
|
function isUser(ty) { return ty.kind === "user"; }
|
|
64
|
-
// ── Method lookup (uses active options) ─────────────────────
|
|
65
|
-
function lookupDotMethod(recvTy, method) {
|
|
66
|
-
return _opts.dotMethods[recvTy.kind]?.[method];
|
|
67
|
-
}
|
|
68
140
|
/** Check if transformed lambda body contains monadic binds. */
|
|
69
141
|
function isMonadicBody(stmts) {
|
|
70
142
|
for (const s of stmts) {
|
|
@@ -84,32 +156,20 @@ function isMonadicBody(stmts) {
|
|
|
84
156
|
}
|
|
85
157
|
return false;
|
|
86
158
|
}
|
|
87
|
-
/** Lean modules that don't need explicit imports. */
|
|
88
|
-
const BUILTIN_MODULES = new Set(["Array", "String", "List", "Nat", "Int"]);
|
|
89
|
-
/** Map from Lean module prefix → import path. */
|
|
90
|
-
const MODULE_IMPORTS = {
|
|
91
|
-
"JSString": "LemmaScript.JSString",
|
|
92
|
-
};
|
|
93
|
-
const usedImports = new Set();
|
|
94
|
-
function lookupMethod(recvTy, method) {
|
|
95
|
-
const tyKey = recvTy.kind === "array" ? "array" : recvTy.kind;
|
|
96
|
-
const lean = _opts.methodTable[tyKey]?.[method];
|
|
97
|
-
if (lean) {
|
|
98
|
-
const mod = lean.split(".")[0];
|
|
99
|
-
if (!BUILTIN_MODULES.has(mod))
|
|
100
|
-
usedImports.add(mod);
|
|
101
|
-
}
|
|
102
|
-
return lean;
|
|
103
|
-
}
|
|
104
159
|
// ── Transform expressions ────────────────────────────────────
|
|
160
|
+
/** Prop-valued operators (for specs/invariants). */
|
|
105
161
|
const OP_MAP = {
|
|
106
162
|
"===": "=", "!==": "≠", ">=": "≥", "<=": "≤", ">": ">", "<": "<",
|
|
107
163
|
"&&": "∧", "||": "∨", "+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
|
|
108
164
|
"==": "=", "!=": "≠",
|
|
109
165
|
};
|
|
166
|
+
/** Bool-valued operators (for code-level conditions needing Decidable). */
|
|
167
|
+
const BOOL_OP_MAP = {
|
|
168
|
+
...OP_MAP, "===": "==", "!==": "!=",
|
|
169
|
+
};
|
|
110
170
|
function transformExpr(e) { return lowerExpr(e, null); }
|
|
111
171
|
/**
|
|
112
|
-
* Lower a typed expression to
|
|
172
|
+
* Lower a typed expression to Backend IR.
|
|
113
173
|
*
|
|
114
174
|
* When `binds` is non-null, embedded method calls are extracted into
|
|
115
175
|
* `let ← ` binds (monadic lifting / selective ANF). Lifting propagates
|
|
@@ -139,6 +199,20 @@ function lowerExpr(e, binds) {
|
|
|
139
199
|
case "unop":
|
|
140
200
|
if (e.op === "-" && e.expr.kind === "num")
|
|
141
201
|
return { kind: "num", value: -e.expr.value };
|
|
202
|
+
// String truthiness: !str → str == ""
|
|
203
|
+
if (e.op === "!" && e.expr.ty.kind === "string")
|
|
204
|
+
return { kind: "binop", op: "=", left: lowerExpr(e.expr, binds), right: { kind: "str", value: "" } };
|
|
205
|
+
// Optional truthiness: !opt → opt is None
|
|
206
|
+
if (e.op === "!" && e.expr.ty.kind === "optional") {
|
|
207
|
+
const bound = matchBinder("value");
|
|
208
|
+
return {
|
|
209
|
+
kind: "match", scrutinee: lowerExpr(e.expr, binds),
|
|
210
|
+
arms: [
|
|
211
|
+
{ pattern: `.some ${bound}`, body: { kind: "bool", value: false } },
|
|
212
|
+
{ pattern: ".none", body: { kind: "bool", value: true } },
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
142
216
|
return { kind: "unop", op: e.op === "!" ? "¬" : e.op, expr: lowerExpr(e.expr, binds) };
|
|
143
217
|
case "binop": {
|
|
144
218
|
// Implication: flatten (A && B) ==> C → implies [A, B] C
|
|
@@ -166,6 +240,89 @@ function lowerExpr(e, binds) {
|
|
|
166
240
|
: { kind: "str", value: e.right.value };
|
|
167
241
|
return { kind: "binop", op: e.op === "===" ? "=" : "≠", left, right };
|
|
168
242
|
}
|
|
243
|
+
// Optional comparison: optExpr op val → match optExpr { Some(v) => v op val, None => false/true }
|
|
244
|
+
if (["===", "!==", ">=", "<=", ">", "<"].includes(e.op) &&
|
|
245
|
+
(e.left.ty.kind === "optional") !== (e.right.ty.kind === "optional")) {
|
|
246
|
+
const [optSide, valSide] = e.left.ty.kind === "optional" ? [e.left, e.right] : [e.right, e.left];
|
|
247
|
+
const optExpr = lowerExpr(optSide, binds);
|
|
248
|
+
// x === undefined → None?, x !== undefined → Some?
|
|
249
|
+
if (valSide.kind === "var" && valSide.name === "undefined") {
|
|
250
|
+
const isNone = e.op === "===";
|
|
251
|
+
return {
|
|
252
|
+
kind: "match", scrutinee: optExpr,
|
|
253
|
+
arms: [
|
|
254
|
+
{ pattern: ".some _", body: { kind: "bool", value: !isNone } },
|
|
255
|
+
{ pattern: ".none", body: { kind: "bool", value: isNone } },
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const valExpr = lowerExpr(valSide, binds);
|
|
260
|
+
const cmpOp = BOOL_OP_MAP[e.op] ?? e.op;
|
|
261
|
+
const noneVal = e.op === "!==" ? true : false;
|
|
262
|
+
const bound = matchBinder("value");
|
|
263
|
+
return {
|
|
264
|
+
kind: "match", scrutinee: optExpr,
|
|
265
|
+
arms: [
|
|
266
|
+
{ pattern: `.some ${bound}`, body: { kind: "binop", op: cmpOp, left: { kind: "var", name: bound }, right: valExpr } },
|
|
267
|
+
{ pattern: ".none", body: { kind: "bool", value: noneVal } },
|
|
268
|
+
],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// || undefined on optional → identity (no-op: x || undefined = x)
|
|
272
|
+
if (e.op === "||" && e.left.ty.kind === "optional" &&
|
|
273
|
+
e.right.kind === "var" && e.right.name === "undefined") {
|
|
274
|
+
return lowerExpr(e.left, binds);
|
|
275
|
+
}
|
|
276
|
+
// || on optional → match Some/None with default
|
|
277
|
+
if (e.op === "||" && e.left.ty.kind === "optional") {
|
|
278
|
+
const optExpr = lowerExpr(e.left, binds);
|
|
279
|
+
const defaultExpr = lowerExpr(e.right, binds);
|
|
280
|
+
const bound = matchBinder("value");
|
|
281
|
+
return {
|
|
282
|
+
kind: "match", scrutinee: optExpr,
|
|
283
|
+
arms: [
|
|
284
|
+
{ pattern: `.some ${bound}`, body: { kind: "var", name: bound } },
|
|
285
|
+
{ pattern: ".none", body: defaultExpr },
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// || on map index → if key in map then map[key] else default
|
|
290
|
+
if (e.op === "||" && e.left.kind === "index" && e.left.obj.ty.kind === "map") {
|
|
291
|
+
const map = lowerExpr(e.left.obj, binds);
|
|
292
|
+
const key = lowerExpr(e.left.idx, binds);
|
|
293
|
+
const right = lowerExpr(e.right, binds);
|
|
294
|
+
return {
|
|
295
|
+
kind: "if",
|
|
296
|
+
cond: { kind: "binop", op: "in", left: key, right: map },
|
|
297
|
+
then: { kind: "index", arr: map, idx: key }, else: right,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// || on non-optional string/array/user → if non-empty then x else default
|
|
301
|
+
if (e.op === "||" && (e.left.ty.kind === "string" || e.left.ty.kind === "array" ||
|
|
302
|
+
(e.left.ty.kind === "user" && e.right.ty.kind === "string"))) {
|
|
303
|
+
const left = lowerExpr(e.left, binds);
|
|
304
|
+
const right = lowerExpr(e.right, binds);
|
|
305
|
+
return {
|
|
306
|
+
kind: "if",
|
|
307
|
+
cond: { kind: "binop", op: ">", left: { kind: "field", obj: left, field: "size" }, right: { kind: "num", value: 0 } },
|
|
308
|
+
then: left, else: right,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// int + string → NatToString(int) + string (string concatenation)
|
|
312
|
+
if (e.op === "+" && _opts.backend === "dafny") {
|
|
313
|
+
const isIntL = e.left.ty.kind === "int" || e.left.ty.kind === "nat";
|
|
314
|
+
const isIntR = e.right.ty.kind === "int" || e.right.ty.kind === "nat";
|
|
315
|
+
if (isIntL && e.right.ty.kind === "string") {
|
|
316
|
+
return { kind: "binop", op: "+",
|
|
317
|
+
left: { kind: "app", fn: "NatToString", args: [lowerExpr(e.left, binds)] },
|
|
318
|
+
right: lowerExpr(e.right, binds) };
|
|
319
|
+
}
|
|
320
|
+
if (e.left.ty.kind === "string" && isIntR) {
|
|
321
|
+
return { kind: "binop", op: "+",
|
|
322
|
+
left: lowerExpr(e.left, binds),
|
|
323
|
+
right: { kind: "app", fn: "NatToString", args: [lowerExpr(e.right, binds)] } };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
169
326
|
return {
|
|
170
327
|
kind: "binop",
|
|
171
328
|
op: OP_MAP[e.op] ?? e.op,
|
|
@@ -178,6 +335,8 @@ function lowerExpr(e, binds) {
|
|
|
178
335
|
return { kind: "field", obj: transformExpr(e.obj), field: "size" };
|
|
179
336
|
if (e.field === "length" && e.obj.ty.kind === "string")
|
|
180
337
|
return { kind: "field", obj: transformExpr(e.obj), field: "length" };
|
|
338
|
+
if (e.field === "size" && (e.obj.ty.kind === "map" || e.obj.ty.kind === "set"))
|
|
339
|
+
return { kind: "field", obj: transformExpr(e.obj), field: "collectionSize" };
|
|
181
340
|
return { kind: "field", obj: transformExpr(e.obj), field: e.field };
|
|
182
341
|
case "index": {
|
|
183
342
|
const idx = transformExpr(e.idx);
|
|
@@ -185,61 +344,198 @@ function lowerExpr(e, binds) {
|
|
|
185
344
|
return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
|
|
186
345
|
}
|
|
187
346
|
case "call": {
|
|
188
|
-
// Math.
|
|
347
|
+
// Math.abs/min/max → preamble functions
|
|
348
|
+
if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math") {
|
|
349
|
+
if (e.fn.field === "abs" && e.args.length === 1)
|
|
350
|
+
return { kind: "app", fn: "MathAbs", args: [lowerExpr(e.args[0], binds)] };
|
|
351
|
+
if (e.fn.field === "min" && e.args.length === 2)
|
|
352
|
+
return { kind: "app", fn: "MathMin", args: e.args.map(a => lowerExpr(a, binds)) };
|
|
353
|
+
if (e.fn.field === "max" && e.args.length === 2)
|
|
354
|
+
return { kind: "app", fn: "MathMax", args: e.args.map(a => lowerExpr(a, binds)) };
|
|
355
|
+
}
|
|
356
|
+
// Math.ceil(x): CeilReal on real args, identity on int
|
|
357
|
+
if (e.fn.kind === "field" && e.fn.field === "ceil" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
|
|
358
|
+
const arg = e.args[0];
|
|
359
|
+
if (arg.ty.kind === "real")
|
|
360
|
+
return { kind: "app", fn: "CeilReal", args: [lowerExpr(arg, binds)] };
|
|
361
|
+
return lowerExpr(arg, binds);
|
|
362
|
+
}
|
|
363
|
+
// Math.floor(x): FloorReal on real args, JSFloorDiv for int division, identity on int
|
|
189
364
|
if (e.fn.kind === "field" && e.fn.field === "floor" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
|
|
190
365
|
const arg = e.args[0];
|
|
366
|
+
if (arg.ty.kind === "real")
|
|
367
|
+
return { kind: "app", fn: "FloorReal", args: [lowerExpr(arg, binds)] };
|
|
191
368
|
if (_opts.backend === "dafny" && arg.kind === "binop" && arg.op === "/")
|
|
192
369
|
return { kind: "app", fn: "JSFloorDiv", args: [lowerExpr(arg.left, binds), lowerExpr(arg.right, binds)] };
|
|
193
370
|
return lowerExpr(arg, binds);
|
|
194
371
|
}
|
|
195
|
-
//
|
|
372
|
+
// Method call: receiver.method(args) → methodCall node
|
|
196
373
|
if (e.fn.kind === "field") {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const name = `_t${_liftCounter++}`;
|
|
219
|
-
binds.push({ kind: "let-bind", name, value: result });
|
|
220
|
-
return { kind: "var", name };
|
|
221
|
-
}
|
|
222
|
-
return result;
|
|
374
|
+
const recv = lowerExpr(e.fn.obj, binds);
|
|
375
|
+
let method = e.fn.field;
|
|
376
|
+
const args = e.args.map((a, i) => {
|
|
377
|
+
const lowered = lowerExpr(a, binds);
|
|
378
|
+
// arr.with index (first arg) needs .toNat when Int-typed
|
|
379
|
+
if (e.fn.kind === "field" && e.fn.field === "with" && e.fn.obj.ty.kind === "array" && i === 0 && !isNat(a.ty))
|
|
380
|
+
return { kind: "toNat", expr: lowered };
|
|
381
|
+
return lowered;
|
|
382
|
+
});
|
|
383
|
+
// Spec-context map get: result type is non-optional → direct access
|
|
384
|
+
if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
|
|
385
|
+
method = "getDirect";
|
|
386
|
+
}
|
|
387
|
+
// Check if any lambda arg has monadic body
|
|
388
|
+
const needsMonadic = _opts.monadic && args.some(a => a.kind === "lambda" && isMonadicBody(a.body));
|
|
389
|
+
const result = { kind: "methodCall", obj: recv, objTy: e.fn.obj.ty, method, args, monadic: needsMonadic };
|
|
390
|
+
// Monadic HOF call is itself monadic — lift via binds like a method call
|
|
391
|
+
if (_opts.monadic && needsMonadic && binds) {
|
|
392
|
+
const name = `_t${_liftCounter++}`;
|
|
393
|
+
binds.push({ kind: "let-bind", name, value: result });
|
|
394
|
+
return { kind: "var", name };
|
|
223
395
|
}
|
|
224
|
-
|
|
396
|
+
return result;
|
|
225
397
|
}
|
|
226
398
|
if (e.fn.kind !== "var")
|
|
227
399
|
throw new Error(`Unsupported call expression: ${e.fn.kind}`);
|
|
228
400
|
const prefix = e.callKind === "spec-pure" && _opts.backend === "lean" ? "Pure." : "";
|
|
229
401
|
return { kind: "app", fn: prefix + e.fn.name, args: e.args.map(a => lowerExpr(a, binds)) };
|
|
230
402
|
}
|
|
231
|
-
case "record":
|
|
232
|
-
|
|
403
|
+
case "record": {
|
|
404
|
+
// Discriminated union: { kind: 'NoOp' } → constructor NoOp
|
|
405
|
+
if (e.ty.kind === "user" && !e.spread) {
|
|
406
|
+
const tyName = e.ty.name;
|
|
407
|
+
// Match base type name (strip generic args: "Result<Model, Err>" → "Result")
|
|
408
|
+
const baseName = tyName.includes("<") ? tyName.slice(0, tyName.indexOf("<")) : tyName;
|
|
409
|
+
const decl = _typeDecls.find(d => d.name === baseName && (d.kind === "discriminated-union" || d.kind === "string-union"));
|
|
410
|
+
if (decl && decl.discriminant) {
|
|
411
|
+
const discField = e.fields.find(f => f.name === decl.discriminant);
|
|
412
|
+
if (discField && (discField.value.kind === "str" || discField.value.kind === "bool")) {
|
|
413
|
+
const variantName = String(discField.value.kind === "str" ? discField.value.value : discField.value.value);
|
|
414
|
+
const variant = decl.variants?.find(v => v.name === variantName);
|
|
415
|
+
if (variant) {
|
|
416
|
+
const nonDiscFields = e.fields.filter(f => f.name !== decl.discriminant);
|
|
417
|
+
if (nonDiscFields.length === 0) {
|
|
418
|
+
return { kind: "constructor", name: variantName, type: tyName };
|
|
419
|
+
}
|
|
420
|
+
// Constructor with args: match variant field order
|
|
421
|
+
const args = variant.fields.map(vf => {
|
|
422
|
+
const ef = nonDiscFields.find(f => f.name === vf.name);
|
|
423
|
+
return ef ? lowerExpr(ef.value, binds) : { kind: "var", name: "None" };
|
|
424
|
+
});
|
|
425
|
+
return { kind: "app", fn: variantName, args };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// For spread records, wrap non-optional values in Some for optional fields
|
|
431
|
+
if (e.spread) {
|
|
432
|
+
const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
|
|
433
|
+
const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
|
|
434
|
+
const structDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "record") : undefined;
|
|
435
|
+
const loweredFields = e.fields.map(f => {
|
|
436
|
+
let value = lowerExpr(f.value, binds);
|
|
437
|
+
if (structDecl?.fields) {
|
|
438
|
+
const fieldDecl = structDecl.fields.find(sf => sf.name === f.name);
|
|
439
|
+
if (fieldDecl) {
|
|
440
|
+
const fieldTy = parseTsType(fieldDecl.tsType);
|
|
441
|
+
const isUndef = f.value.kind === "var" && f.value.name === "undefined";
|
|
442
|
+
if (fieldTy.kind === "optional" && f.value.ty.kind !== "optional" && !isUndef) {
|
|
443
|
+
value = { kind: "app", fn: "Some", args: [value] };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { name: f.name, value };
|
|
448
|
+
});
|
|
449
|
+
return { kind: "record", spread: lowerExpr(e.spread, binds), fields: loweredFields };
|
|
450
|
+
}
|
|
451
|
+
return { kind: "record", spread: null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
|
|
452
|
+
}
|
|
233
453
|
case "arrayLiteral":
|
|
454
|
+
if (e.ty.kind === "map" && e.elems.length === 0)
|
|
455
|
+
return { kind: "emptyMap" };
|
|
456
|
+
if (e.ty.kind === "set" && e.elems.length === 0)
|
|
457
|
+
return { kind: "emptySet" };
|
|
458
|
+
// Set with initial elements: new Set([a, b]) → {a, b}
|
|
459
|
+
if (e.ty.kind === "set")
|
|
460
|
+
return { kind: "app", fn: "SetLiteral", args: e.elems.map(el => lowerExpr(el, binds)) };
|
|
234
461
|
return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
|
|
235
462
|
case "lambda":
|
|
236
|
-
return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type:
|
|
463
|
+
return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: p.ty })), body: transformStmts(e.body, []) };
|
|
237
464
|
case "forall":
|
|
238
|
-
return { kind: "forall", var: e.var, type:
|
|
465
|
+
return { kind: "forall", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
239
466
|
case "exists":
|
|
240
|
-
return { kind: "exists", var: e.var, type:
|
|
241
|
-
case "conditional":
|
|
242
|
-
|
|
467
|
+
return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
468
|
+
case "conditional": {
|
|
469
|
+
const cond = lowerExpr(e.cond, binds);
|
|
470
|
+
let thenExpr = lowerExpr(e.then, binds);
|
|
471
|
+
let elseExpr = lowerExpr(e.else, binds);
|
|
472
|
+
// Explicit !== undefined with narrowedExpr → match Some/None on the optional expression
|
|
473
|
+
if (e.narrowedVar && e.narrowedExpr) {
|
|
474
|
+
const scrutinee = lowerExpr(e.narrowedExpr, binds);
|
|
475
|
+
const bound = matchBinder(e.narrowedVar);
|
|
476
|
+
if (bound !== e.narrowedVar) {
|
|
477
|
+
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
478
|
+
}
|
|
479
|
+
const wrapSomeNone = (expr, raw) => (raw.kind === "var" && raw.name === "undefined")
|
|
480
|
+
? { kind: "constructor", name: ".none" }
|
|
481
|
+
: { kind: "app", fn: "Some", args: [expr] };
|
|
482
|
+
thenExpr = wrapSomeNone(thenExpr, e.then);
|
|
483
|
+
elseExpr = wrapSomeNone(elseExpr, e.else);
|
|
484
|
+
return {
|
|
485
|
+
kind: "match", scrutinee,
|
|
486
|
+
arms: [
|
|
487
|
+
{ pattern: `.some ${bound}`, body: thenExpr },
|
|
488
|
+
{ pattern: ".none", body: elseExpr },
|
|
489
|
+
],
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// Optional cond with narrowedVar → match Some/None (truthiness)
|
|
493
|
+
if (e.narrowedVar && e.cond.ty.kind === "optional") {
|
|
494
|
+
const bound = matchBinder(e.narrowedVar);
|
|
495
|
+
// Replace the synthetic/narrowed var with the match-bound name
|
|
496
|
+
if (bound !== e.narrowedVar) {
|
|
497
|
+
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
498
|
+
}
|
|
499
|
+
// The match produces an Optional: wrap branches in Some/None.
|
|
500
|
+
// Either branch being undefined signals None; otherwise wrap in Some.
|
|
501
|
+
const wrapSomeNone = (expr, raw) => (raw.kind === "var" && raw.name === "undefined")
|
|
502
|
+
? { kind: "constructor", name: ".none" }
|
|
503
|
+
: { kind: "app", fn: "Some", args: [expr] };
|
|
504
|
+
thenExpr = wrapSomeNone(thenExpr, e.then);
|
|
505
|
+
elseExpr = wrapSomeNone(elseExpr, e.else);
|
|
506
|
+
return {
|
|
507
|
+
kind: "match", scrutinee: cond,
|
|
508
|
+
arms: [
|
|
509
|
+
{ pattern: `.some ${bound}`, body: thenExpr },
|
|
510
|
+
{ pattern: ".none", body: elseExpr },
|
|
511
|
+
],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
// Non-optional: regular if with optional wrapping
|
|
515
|
+
if (e.ty.kind === "optional") {
|
|
516
|
+
if (e.then.kind === "var" && e.then.name === "undefined") {
|
|
517
|
+
thenExpr = { kind: "constructor", name: ".none" };
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
thenExpr = { kind: "app", fn: "Some", args: [thenExpr] };
|
|
521
|
+
}
|
|
522
|
+
if (e.else.kind === "var" && e.else.name === "undefined") {
|
|
523
|
+
elseExpr = { kind: "constructor", name: ".none" };
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
elseExpr = { kind: "app", fn: "Some", args: [elseExpr] };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return { kind: "if", cond, then: thenExpr, else: elseExpr };
|
|
530
|
+
}
|
|
531
|
+
case "havoc":
|
|
532
|
+
// Dafny's * only works in var/assign positions — lift to own declaration
|
|
533
|
+
if (binds) {
|
|
534
|
+
const name = `_t${_liftCounter++}`;
|
|
535
|
+
binds.push({ kind: "let", name, type: e.ty, mutable: false, value: { kind: "havoc", type: e.ty } });
|
|
536
|
+
return { kind: "var", name };
|
|
537
|
+
}
|
|
538
|
+
return { kind: "havoc", type: e.ty };
|
|
243
539
|
}
|
|
244
540
|
}
|
|
245
541
|
function flattenImpl(e) {
|
|
@@ -275,37 +571,23 @@ function ensuresToMatch(e, typeDecls) {
|
|
|
275
571
|
if (!variant)
|
|
276
572
|
return null;
|
|
277
573
|
const fields = variant.fields;
|
|
278
|
-
const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${variantName}`;
|
|
574
|
+
const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name, obj.name)).join(" ")}` : `.${variantName}`;
|
|
279
575
|
let rhs = transformExpr(e.right);
|
|
280
576
|
rhs = replaceFieldAccess(rhs, obj.name, fields);
|
|
281
577
|
return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
|
|
282
578
|
}
|
|
283
579
|
function replaceFieldAccess(e, varName, fields) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
case "exists": return { ...e, body: r(e.body) };
|
|
296
|
-
case "app": return { ...e, args: e.args.map(r) };
|
|
297
|
-
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(f => ({ ...f, value: r(f.value) })) };
|
|
298
|
-
case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
|
|
299
|
-
case "let":
|
|
300
|
-
// If this let shadows the matched variable, stop replacing in the body
|
|
301
|
-
if (e.name === varName)
|
|
302
|
-
return { ...e, value: r(e.value) };
|
|
303
|
-
return { ...e, value: r(e.value), body: r(e.body) };
|
|
304
|
-
case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
|
|
305
|
-
case "field": return { ...e, obj: r(e.obj) };
|
|
306
|
-
case "match": return { ...e, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
|
|
307
|
-
default: return e;
|
|
308
|
-
}
|
|
580
|
+
return mapExpr(e, x => {
|
|
581
|
+
if (x.kind === "field" && x.obj.kind === "var" && x.obj.name === varName) {
|
|
582
|
+
const f = fields.find(f => f.name === x.field);
|
|
583
|
+
if (f)
|
|
584
|
+
return { kind: "var", name: matchBinder(f.name, varName) };
|
|
585
|
+
}
|
|
586
|
+
// If this let shadows the matched variable, stop replacing in the body
|
|
587
|
+
if (x.kind === "let" && x.name === varName)
|
|
588
|
+
return { ...x, value: replaceFieldAccess(x.value, varName, fields) };
|
|
589
|
+
return null;
|
|
590
|
+
});
|
|
309
591
|
}
|
|
310
592
|
// ── Transform statements ─────────────────────────────────────
|
|
311
593
|
function transformStmts(stmts, typeDecls) {
|
|
@@ -321,20 +603,76 @@ function transformStmts(stmts, typeDecls) {
|
|
|
321
603
|
i += chain.consumed;
|
|
322
604
|
continue;
|
|
323
605
|
}
|
|
606
|
+
// Detect optional check → match on Some/None
|
|
607
|
+
const opt = parseOptionalCheck(s.cond);
|
|
608
|
+
if (opt) {
|
|
609
|
+
const rest = stmts.slice(i + 1);
|
|
610
|
+
result.push(emitOptionalMatch(opt.varName, opt.negated, s, typeDecls, rest));
|
|
611
|
+
// If rest was consumed into the Some branch, skip remaining
|
|
612
|
+
const someBranch = opt.negated ? s.else : s.then;
|
|
613
|
+
if (someBranch.length === 0 && rest.length > 0) {
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
616
|
+
i++;
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
324
619
|
}
|
|
325
620
|
// Transform for-of → for-in over range
|
|
326
621
|
if (s.kind === "forof") {
|
|
327
|
-
const
|
|
328
|
-
const
|
|
622
|
+
const varName = s.names[0];
|
|
623
|
+
const varTy = s.nameTypes[0] ?? { kind: "unknown" };
|
|
624
|
+
let iterExpr = transformExpr(s.iterable);
|
|
625
|
+
// Map iteration: for (const [k, v] of map) → iterate keys, look up values
|
|
626
|
+
if (s.names.length >= 2 && s.iterable.ty.kind === "map") {
|
|
627
|
+
const keyName = s.names[0], valueName = s.names[1];
|
|
628
|
+
const keyTy = s.nameTypes[0] ?? { kind: "unknown" };
|
|
629
|
+
const valueTy = s.nameTypes[1] ?? { kind: "unknown" };
|
|
630
|
+
const keysSeqName = `_${keyName}_keys`;
|
|
631
|
+
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
632
|
+
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
633
|
+
const keysVar = { kind: "var", name: keysSeqName };
|
|
634
|
+
const count = _forofCounters.get(keyName) ?? 0;
|
|
635
|
+
_forofCounters.set(keyName, count + 1);
|
|
636
|
+
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
637
|
+
const idxName = `_${keyName}_idx${suffix}`;
|
|
638
|
+
const idx = { kind: "var", name: idxName };
|
|
639
|
+
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
640
|
+
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
641
|
+
const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
|
|
642
|
+
const letVal = { kind: "let", name: valueName, type: valueTy, mutable: false,
|
|
643
|
+
value: { kind: "methodCall", obj: iterExpr, objTy: s.iterable.ty, method: "getDirect", args: [{ kind: "var", name: keyName }], monadic: false } };
|
|
644
|
+
const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
|
|
645
|
+
result.push({
|
|
646
|
+
kind: "forin", idx: idxName, bound: arrSize,
|
|
647
|
+
invariants: [boundInv, ...s.invariants.map(transformExpr)],
|
|
648
|
+
body: [letKey, letVal, ...bodyStmts],
|
|
649
|
+
});
|
|
650
|
+
i++;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
// Sets aren't indexable — bind SetToSeq to a variable for iteration
|
|
654
|
+
if (s.iterable.ty.kind === "set") {
|
|
655
|
+
const seqName = `_${varName}_seq`;
|
|
656
|
+
const convExpr = { kind: "app", fn: "SetToSeq", args: [iterExpr] };
|
|
657
|
+
const elemTy = varTy.kind !== "unknown" ? varTy : { kind: "string" };
|
|
658
|
+
result.push({ kind: "let", name: seqName, type: { kind: "array", elem: elemTy }, mutable: false, value: convExpr });
|
|
659
|
+
iterExpr = { kind: "var", name: seqName };
|
|
660
|
+
}
|
|
661
|
+
const count = _forofCounters.get(varName) ?? 0;
|
|
662
|
+
_forofCounters.set(varName, count + 1);
|
|
663
|
+
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
664
|
+
const idxName = `_${varName}_idx${suffix}`;
|
|
329
665
|
const idx = { kind: "var", name: idxName };
|
|
330
|
-
const arrSize = { kind: "field", obj:
|
|
666
|
+
const arrSize = { kind: "field", obj: iterExpr, field: "size" };
|
|
331
667
|
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
332
|
-
const letElem = { kind: "let", name:
|
|
668
|
+
const letElem = { kind: "let", name: varName, type: varTy, mutable: false, value: { kind: "index", arr: iterExpr, idx } };
|
|
669
|
+
// Auto-add bound invariant: idx ≤ bound (always true for range loops)
|
|
670
|
+
const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
|
|
333
671
|
result.push({
|
|
334
672
|
kind: "forin",
|
|
335
673
|
idx: idxName,
|
|
336
674
|
bound: arrSize,
|
|
337
|
-
invariants: s.invariants.map(transformExpr),
|
|
675
|
+
invariants: [boundInv, ...s.invariants.map(transformExpr)],
|
|
338
676
|
body: [letElem, ...bodyStmts],
|
|
339
677
|
});
|
|
340
678
|
i++;
|
|
@@ -354,8 +692,71 @@ function liftMethodCalls(e) {
|
|
|
354
692
|
function transformStmt(s, typeDecls) {
|
|
355
693
|
switch (s.kind) {
|
|
356
694
|
case "let": {
|
|
695
|
+
// Whole-init havoc: emit directly, no lifting
|
|
696
|
+
if (s.init.kind === "havoc") {
|
|
697
|
+
return [{ kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: { kind: "havoc", type: s.init.ty } }];
|
|
698
|
+
}
|
|
699
|
+
// arr.shift()! → let x = arr[0]; arr = arr[1..]
|
|
700
|
+
const init = s.init.kind === "call" ? s.init : undefined;
|
|
701
|
+
if (init && init.fn.kind === "field" && init.fn.field === "shift" && init.fn.obj.ty.kind === "array") {
|
|
702
|
+
const arrName = init.fn.obj.kind === "var" ? init.fn.obj.name : undefined;
|
|
703
|
+
if (arrName) {
|
|
704
|
+
const arrVar = { kind: "var", name: arrName };
|
|
705
|
+
const letHead = { kind: "let", name: s.name, type: s.ty, mutable: s.mutable,
|
|
706
|
+
value: { kind: "index", arr: arrVar, idx: { kind: "num", value: 0 } } };
|
|
707
|
+
const sliceTail = { kind: "assign", target: arrName,
|
|
708
|
+
value: { kind: "methodCall", obj: arrVar, objTy: init.fn.obj.ty, method: "slice", args: [{ kind: "num", value: 1 }], monadic: false } };
|
|
709
|
+
return [letHead, sliceTail];
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// new Map(arr.map(n => [n.field, n])) → let m = map[]; for (n of arr) m[n.field] := n
|
|
713
|
+
if (init && init.fn.kind === "var" && init.fn.name === "__mapFromArray" &&
|
|
714
|
+
init.args.length === 1 && init.args[0].kind === "call" &&
|
|
715
|
+
init.args[0].fn.kind === "field" && init.args[0].fn.field === "map" &&
|
|
716
|
+
init.args[0].args.length === 1 && init.args[0].args[0].kind === "lambda") {
|
|
717
|
+
const arrExpr = init.args[0].fn.obj;
|
|
718
|
+
const lam = init.args[0].args[0];
|
|
719
|
+
const param = lam.params[0]?.name ?? "_";
|
|
720
|
+
const lamBody = Array.isArray(lam.body) ? lam.body : [{ kind: "return", value: lam.body }];
|
|
721
|
+
const retStmt = lamBody.find(b => b.kind === "return");
|
|
722
|
+
if (retStmt && retStmt.value.kind === "arrayLiteral" && retStmt.value.elems.length === 2) {
|
|
723
|
+
const keyExpr = retStmt.value.elems[0];
|
|
724
|
+
const valExpr = retStmt.value.elems[1];
|
|
725
|
+
const arrIR = transformExpr(arrExpr);
|
|
726
|
+
const arrTy = arrExpr.ty;
|
|
727
|
+
const elemTy = arrTy.kind === "array" ? arrTy.elem : { kind: "unknown" };
|
|
728
|
+
const idxName = `_${param}_idx`;
|
|
729
|
+
const idx = { kind: "var", name: idxName };
|
|
730
|
+
const arrSize = { kind: "field", obj: arrIR, field: "size" };
|
|
731
|
+
const elemVar = { kind: "var", name: param };
|
|
732
|
+
const keyIR = transformExpr(keyExpr);
|
|
733
|
+
const valIR = transformExpr(valExpr);
|
|
734
|
+
const mapSet = { kind: "methodCall", obj: { kind: "var", name: s.name }, objTy: s.ty, method: "set", args: [keyIR, valIR], monadic: false };
|
|
735
|
+
// Auto-invariant: all processed elements' keys are in the map
|
|
736
|
+
const kVar = { kind: "var", name: "ki" };
|
|
737
|
+
const mapHasKey = {
|
|
738
|
+
kind: "implies",
|
|
739
|
+
premises: [
|
|
740
|
+
{ kind: "binop", op: "≥", left: kVar, right: { kind: "num", value: 0 } },
|
|
741
|
+
{ kind: "binop", op: "<", left: kVar, right: idx },
|
|
742
|
+
],
|
|
743
|
+
conclusion: { kind: "methodCall", obj: { kind: "var", name: s.name }, objTy: s.ty, method: "has", args: [keyIR.kind === "field" ? { kind: "field", obj: { kind: "index", arr: arrIR, idx: kVar }, field: keyIR.field } : keyIR], monadic: false },
|
|
744
|
+
};
|
|
745
|
+
const autoInv = { kind: "forall", var: "ki", type: { kind: "int" }, body: mapHasKey };
|
|
746
|
+
const stmts = [
|
|
747
|
+
{ kind: "let", name: s.name, type: s.ty, mutable: true, value: { kind: "emptyMap" } },
|
|
748
|
+
{ kind: "forin", idx: idxName, bound: arrSize,
|
|
749
|
+
invariants: [{ kind: "binop", op: "≤", left: idx, right: arrSize }, autoInv],
|
|
750
|
+
body: [
|
|
751
|
+
{ kind: "let", name: param, type: elemTy, mutable: false, value: { kind: "index", arr: arrIR, idx } },
|
|
752
|
+
{ kind: "assign", target: s.name, value: mapSet },
|
|
753
|
+
] },
|
|
754
|
+
];
|
|
755
|
+
return stmts;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
357
758
|
const { binds, expr } = liftMethodCalls(s.init);
|
|
358
|
-
return [...binds, { kind: "let", name: s.name, type:
|
|
759
|
+
return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
|
|
359
760
|
}
|
|
360
761
|
case "assign": {
|
|
361
762
|
// Top-level method call → direct monadic bind, no lifting needed
|
|
@@ -371,10 +772,52 @@ function transformStmt(s, typeDecls) {
|
|
|
371
772
|
case "break": return [{ kind: "break" }];
|
|
372
773
|
case "continue": return [{ kind: "continue" }];
|
|
373
774
|
case "expr": {
|
|
775
|
+
// Mutating collection call: m.set(k, v) → m := m.set(k, v)
|
|
776
|
+
// Same for s.add(x) on sets, arr.push(x)
|
|
777
|
+
if (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
|
|
778
|
+
s.expr.fn.obj.kind === "var" &&
|
|
779
|
+
((s.expr.fn.obj.ty.kind === "map" || s.expr.fn.obj.ty.kind === "set") &&
|
|
780
|
+
(s.expr.fn.field === "set" || s.expr.fn.field === "add" || s.expr.fn.field === "delete")) ||
|
|
781
|
+
(s.expr.kind === "call" && s.expr.fn.kind === "field" &&
|
|
782
|
+
s.expr.fn.obj.kind === "var" && s.expr.fn.obj.ty.kind === "array" &&
|
|
783
|
+
s.expr.fn.field === "push")) {
|
|
784
|
+
const receiver = s.expr.fn.obj.name;
|
|
785
|
+
const { binds, expr } = liftMethodCalls(s.expr);
|
|
786
|
+
return [...binds, { kind: "assign", target: receiver, value: expr }];
|
|
787
|
+
}
|
|
788
|
+
// Optional chaining on map.get: m.get(k)?.push(v) → if k in m { m[k] := m[k] + [v] }
|
|
789
|
+
if (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
|
|
790
|
+
s.expr.fn.obj.kind === "call" && s.expr.fn.obj.fn.kind === "field" &&
|
|
791
|
+
s.expr.fn.obj.fn.obj.ty.kind === "map" && s.expr.fn.obj.fn.field === "get" &&
|
|
792
|
+
s.expr.fn.field === "push") {
|
|
793
|
+
const mapExpr = s.expr.fn.obj.fn.obj;
|
|
794
|
+
const mapName = mapExpr.kind === "var" ? mapExpr.name : undefined;
|
|
795
|
+
const keyExpr = lowerExpr(s.expr.fn.obj.args[0], null);
|
|
796
|
+
const pushArg = lowerExpr(s.expr.args[0], null);
|
|
797
|
+
if (mapName) {
|
|
798
|
+
const mapVar = { kind: "var", name: mapName };
|
|
799
|
+
const directGet = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "getDirect", args: [keyExpr], monadic: false };
|
|
800
|
+
const pushed = { kind: "methodCall", obj: directGet, objTy: mapExpr.ty.value, method: "push", args: [pushArg], monadic: false };
|
|
801
|
+
const updated = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "set", args: [keyExpr, pushed], monadic: false };
|
|
802
|
+
const hasCond = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "has", args: [keyExpr], monadic: false };
|
|
803
|
+
return [{ kind: "if", cond: hasCond, then: [{ kind: "assign", target: mapName, value: updated }], else: [] }];
|
|
804
|
+
}
|
|
805
|
+
}
|
|
374
806
|
const { binds, expr } = liftMethodCalls(s.expr);
|
|
375
807
|
return [...binds, { kind: "assign", target: "_", value: expr }];
|
|
376
808
|
}
|
|
377
809
|
case "if": {
|
|
810
|
+
// Restructure && with optional check: extract the leftmost optional check
|
|
811
|
+
// from a && chain and nest the rest inside. Handles left-associative chains:
|
|
812
|
+
// if ((x !== undefined && b) && c) → if (x !== undefined) { if (b && c) { ... } }
|
|
813
|
+
if (s.cond.kind === "binop" && s.cond.op === "&&" && s.else.length === 0) {
|
|
814
|
+
const extracted = extractLeftmostOptional(s.cond);
|
|
815
|
+
if (extracted) {
|
|
816
|
+
const innerIf = { kind: "if", cond: extracted.rest, then: s.then, else: [] };
|
|
817
|
+
const outerIf = { kind: "if", cond: extracted.optCond, then: [innerIf], else: [] };
|
|
818
|
+
return transformStmts([outerIf], typeDecls);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
378
821
|
// Lift from condition only (Lean rule: don't lift from branches)
|
|
379
822
|
const { binds, expr: cond } = liftMethodCalls(s.cond);
|
|
380
823
|
return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
@@ -388,10 +831,18 @@ function transformStmt(s, typeDecls) {
|
|
|
388
831
|
doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
|
|
389
832
|
body: transformStmts(s.body, typeDecls),
|
|
390
833
|
}];
|
|
834
|
+
case "throw":
|
|
835
|
+
return [{ kind: "assert", expr: { kind: "bool", value: false } }];
|
|
391
836
|
case "forof":
|
|
392
837
|
throw new Error("forof should be transformed to forin (range loop) in transformStmts");
|
|
393
838
|
case "switch":
|
|
394
839
|
return [emitSwitchStmt(s, typeDecls)];
|
|
840
|
+
case "ghostLet":
|
|
841
|
+
return [{ kind: "ghostLet", name: s.name, type: s.ty, value: transformExpr(s.init) }];
|
|
842
|
+
case "ghostAssign":
|
|
843
|
+
return [{ kind: "ghostAssign", target: s.target, value: transformExpr(s.value) }];
|
|
844
|
+
case "assert":
|
|
845
|
+
return [{ kind: "assert", expr: transformExpr(s.expr) }];
|
|
395
846
|
}
|
|
396
847
|
}
|
|
397
848
|
function detectDiscriminantChain(stmts) {
|
|
@@ -443,14 +894,65 @@ function parseDiscriminantCond(cond) {
|
|
|
443
894
|
return null;
|
|
444
895
|
return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
445
896
|
}
|
|
897
|
+
function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
|
|
898
|
+
let someBranch = negated ? s.else : s.then;
|
|
899
|
+
const noneBranch = negated ? s.then : s.else;
|
|
900
|
+
// Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
|
|
901
|
+
// so include remaining statements as the Some body
|
|
902
|
+
if (someBranch.length === 0 && restStmts && restStmts.length > 0) {
|
|
903
|
+
someBranch = restStmts;
|
|
904
|
+
}
|
|
905
|
+
const bound = matchBinder(`${varName}_val`);
|
|
906
|
+
const someBody = transformStmts(someBranch, typeDecls);
|
|
907
|
+
const r = (e) => replaceVar(e, varName, { kind: "var", name: bound });
|
|
908
|
+
const someReplaced = someBody.map(stmt => mapStmtExprs(stmt, r));
|
|
909
|
+
const arms = [
|
|
910
|
+
{ pattern: `.some ${bound}`, body: someReplaced },
|
|
911
|
+
{ pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
|
|
912
|
+
];
|
|
913
|
+
return { kind: "match", scrutinee: varName, arms };
|
|
914
|
+
}
|
|
915
|
+
/** Apply an expression transform to all expressions in a statement (convenience wrapper). */
|
|
916
|
+
function mapStmtExprs(s, r) {
|
|
917
|
+
return mapStmt(s, e => r(e));
|
|
918
|
+
}
|
|
919
|
+
/** Extract the leftmost optional check from a && chain, returning the check and the rest.
|
|
920
|
+
* (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
|
|
921
|
+
function extractLeftmostOptional(cond) {
|
|
922
|
+
if (cond.kind !== "binop" || cond.op !== "&&")
|
|
923
|
+
return null;
|
|
924
|
+
const check = parseOptionalCheck(cond.left);
|
|
925
|
+
if (check && !check.negated)
|
|
926
|
+
return { optCond: cond.left, rest: cond.right };
|
|
927
|
+
if (cond.left.kind === "binop" && cond.left.op === "&&") {
|
|
928
|
+
const inner = extractLeftmostOptional(cond.left);
|
|
929
|
+
if (inner)
|
|
930
|
+
return { optCond: inner.optCond, rest: { ...cond, left: inner.rest } };
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
/** Detect `v !== undefined` or `undefined !== v` where v has optional type. */
|
|
935
|
+
function parseOptionalCheck(cond) {
|
|
936
|
+
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
|
|
937
|
+
return null;
|
|
938
|
+
let varExpr = null;
|
|
939
|
+
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
940
|
+
varExpr = cond.left;
|
|
941
|
+
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
942
|
+
varExpr = cond.right;
|
|
943
|
+
if (!varExpr || varExpr.kind !== "var" || varExpr.ty.kind !== "optional")
|
|
944
|
+
return null;
|
|
945
|
+
return { varName: varExpr.name, negated: cond.op === "===" };
|
|
946
|
+
}
|
|
446
947
|
function emitMatchStmt(chain, typeDecls) {
|
|
447
948
|
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
448
949
|
const arms = chain.cases.map(c => {
|
|
449
950
|
const variant = decl?.variants?.find(v => v.name === c.variant);
|
|
450
951
|
const fields = variant?.fields ?? [];
|
|
451
|
-
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
|
|
452
|
-
|
|
453
|
-
|
|
952
|
+
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name, chain.varName)).join(" ")}` : `.${c.variant}`;
|
|
953
|
+
// Replace field accesses in TStmt BEFORE transforming, so optional narrowing sees simple vars
|
|
954
|
+
const replaced = replaceFieldAccessInTStmts(c.body, chain.varName, fields);
|
|
955
|
+
const body = transformStmts(replaced, typeDecls);
|
|
454
956
|
return { pattern, body };
|
|
455
957
|
});
|
|
456
958
|
if (chain.fallthrough.length > 0)
|
|
@@ -464,48 +966,56 @@ function emitSwitchStmt(s, typeDecls) {
|
|
|
464
966
|
const arms = s.cases.map(c => {
|
|
465
967
|
const variant = decl?.variants?.find(v => v.name === c.label);
|
|
466
968
|
const fields = variant?.fields ?? [];
|
|
467
|
-
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
|
|
468
|
-
|
|
469
|
-
|
|
969
|
+
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name, varName)).join(" ")}` : `.${c.label}`;
|
|
970
|
+
// Replace field accesses in TStmt BEFORE transforming, so optional narrowing sees simple vars
|
|
971
|
+
const replaced = replaceFieldAccessInTStmts(c.body, varName, fields);
|
|
972
|
+
const body = transformStmts(replaced, typeDecls);
|
|
470
973
|
return { pattern, body };
|
|
471
974
|
});
|
|
472
975
|
if (s.defaultBody.length > 0)
|
|
473
976
|
arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
|
|
474
977
|
return { kind: "match", scrutinee: varName, arms };
|
|
475
978
|
}
|
|
979
|
+
/** Replace obj.field → binder var in typed IR (before transform).
|
|
980
|
+
* Uses the variant's declared field type since the resolve phase may not
|
|
981
|
+
* resolve field types on discriminated unions correctly. */
|
|
982
|
+
function replaceFieldAccessInTStmts(stmts, varName, fields) {
|
|
983
|
+
if (fields.length === 0)
|
|
984
|
+
return stmts;
|
|
985
|
+
return stmts.map(s => mapTStmt(s, e => {
|
|
986
|
+
if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === varName) {
|
|
987
|
+
const fi = fields.find(fi => fi.name === e.field);
|
|
988
|
+
if (fi) {
|
|
989
|
+
const ty = e.ty.kind !== "unknown" ? e.ty : parseTsType(fi.tsType);
|
|
990
|
+
return { kind: "var", name: matchBinder(fi.name, varName), ty };
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
}));
|
|
995
|
+
}
|
|
476
996
|
function replaceFieldAccessInStmts(stmts, varName, fields) {
|
|
477
997
|
if (fields.length === 0)
|
|
478
998
|
return stmts;
|
|
999
|
+
const f = (e) => {
|
|
1000
|
+
if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === varName) {
|
|
1001
|
+
const fi = fields.find(fi => fi.name === e.field);
|
|
1002
|
+
if (fi)
|
|
1003
|
+
return { kind: "var", name: matchBinder(fi.name, varName) };
|
|
1004
|
+
}
|
|
1005
|
+
return null;
|
|
1006
|
+
};
|
|
479
1007
|
const result = [];
|
|
480
1008
|
for (const s of stmts) {
|
|
481
1009
|
// If a let shadows the matched variable, stop replacing from here on
|
|
482
1010
|
if (s.kind === "let" && s.name === varName) {
|
|
483
|
-
|
|
484
|
-
result.push({ ...s, value: r(s.value) });
|
|
485
|
-
// Remaining statements see the shadowed name — no more replacement
|
|
1011
|
+
result.push(s.value ? { ...s, value: mapExpr(s.value, f) } : s);
|
|
486
1012
|
result.push(...stmts.slice(result.length));
|
|
487
1013
|
break;
|
|
488
1014
|
}
|
|
489
|
-
result.push(
|
|
1015
|
+
result.push(mapStmt(s, f));
|
|
490
1016
|
}
|
|
491
1017
|
return result;
|
|
492
1018
|
}
|
|
493
|
-
function replaceFieldAccessInStmt(s, varName, fields) {
|
|
494
|
-
const r = (e) => replaceFieldAccess(e, varName, fields);
|
|
495
|
-
switch (s.kind) {
|
|
496
|
-
case "let": return { ...s, value: r(s.value) };
|
|
497
|
-
case "assign": return { ...s, value: r(s.value) };
|
|
498
|
-
case "bind": return { ...s, value: r(s.value) };
|
|
499
|
-
case "let-bind": return { ...s, value: r(s.value) };
|
|
500
|
-
case "return": return { ...s, value: r(s.value) };
|
|
501
|
-
case "break":
|
|
502
|
-
case "continue": return s;
|
|
503
|
-
case "if": return { ...s, cond: r(s.cond), then: replaceFieldAccessInStmts(s.then, varName, fields), else: replaceFieldAccessInStmts(s.else, varName, fields) };
|
|
504
|
-
case "match": return { ...s, arms: s.arms.map(a => ({ ...a, body: replaceFieldAccessInStmts(a.body, varName, fields) })) };
|
|
505
|
-
case "while": return { ...s, cond: r(s.cond), body: replaceFieldAccessInStmts(s.body, varName, fields) };
|
|
506
|
-
case "forin": return { ...s, invariants: s.invariants.map(r), body: replaceFieldAccessInStmts(s.body, varName, fields) };
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
1019
|
// ── Pure function generation ─────────────────────────────────
|
|
510
1020
|
function transformPureBody(stmts, typeDecls) {
|
|
511
1021
|
// Detect discriminant if-chain
|
|
@@ -526,6 +1036,29 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
526
1036
|
return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
|
|
527
1037
|
}
|
|
528
1038
|
case "if": {
|
|
1039
|
+
// Optional narrowing: if (x === undefined) → match x { None => ..., Some(x_val) => ... }
|
|
1040
|
+
const optCheck = parseOptionalCheck(s.cond);
|
|
1041
|
+
if (optCheck) {
|
|
1042
|
+
let someBranch = optCheck.negated ? s.else : s.then;
|
|
1043
|
+
const noneBranch = optCheck.negated ? s.then : (s.else.length > 0 ? s.else : rest);
|
|
1044
|
+
if (someBranch.length === 0)
|
|
1045
|
+
someBranch = rest;
|
|
1046
|
+
const bound = matchBinder(`${optCheck.varName}_val`);
|
|
1047
|
+
const someExpr = transformPureBody(someBranch, typeDecls);
|
|
1048
|
+
if (!someExpr)
|
|
1049
|
+
return null;
|
|
1050
|
+
const noneExpr = transformPureBody(noneBranch, typeDecls);
|
|
1051
|
+
if (!noneExpr)
|
|
1052
|
+
return null;
|
|
1053
|
+
const someReplaced = replaceVar(someExpr, optCheck.varName, { kind: "var", name: bound });
|
|
1054
|
+
return {
|
|
1055
|
+
kind: "match", scrutinee: optCheck.varName,
|
|
1056
|
+
arms: [
|
|
1057
|
+
{ pattern: `.some ${bound}`, body: someReplaced },
|
|
1058
|
+
{ pattern: ".none", body: noneExpr },
|
|
1059
|
+
],
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
529
1062
|
const thenExpr = transformPureBody(s.then, typeDecls);
|
|
530
1063
|
if (!thenExpr)
|
|
531
1064
|
return null;
|
|
@@ -545,11 +1078,12 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
545
1078
|
const decl = typeDecls.find(d => d.name === (s.expr.ty.kind === "user" ? s.expr.ty.name : ""));
|
|
546
1079
|
if (!decl)
|
|
547
1080
|
return null;
|
|
1081
|
+
const varName = s.expr.kind === "var" ? s.expr.name : undefined;
|
|
548
1082
|
const arms = [];
|
|
549
1083
|
for (const c of s.cases) {
|
|
550
1084
|
const variant = decl.variants?.find(v => v.name === c.label);
|
|
551
1085
|
const fields = variant?.fields ?? [];
|
|
552
|
-
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
|
|
1086
|
+
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name, varName)).join(" ")}` : `.${c.label}`;
|
|
553
1087
|
let body = transformPureBody(c.body, typeDecls);
|
|
554
1088
|
if (!body)
|
|
555
1089
|
return null;
|
|
@@ -573,7 +1107,7 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
573
1107
|
for (const c of chain.cases) {
|
|
574
1108
|
const variant = decl?.variants?.find(v => v.name === c.variant);
|
|
575
1109
|
const fields = variant?.fields ?? [];
|
|
576
|
-
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
|
|
1110
|
+
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name, chain.varName)).join(" ")}` : `.${c.variant}`;
|
|
577
1111
|
let body = transformPureBody(c.body, typeDecls);
|
|
578
1112
|
if (!body)
|
|
579
1113
|
return null;
|
|
@@ -605,50 +1139,89 @@ function transformTypeDecl(d) {
|
|
|
605
1139
|
else if (d.kind === "discriminated-union") {
|
|
606
1140
|
return {
|
|
607
1141
|
kind: "inductive", name: d.name,
|
|
1142
|
+
typeParams: d.typeParams,
|
|
608
1143
|
constructors: d.variants.map(v => ({
|
|
609
1144
|
name: v.name,
|
|
610
|
-
fields: v.fields.map(f => ({ name: f.name, type:
|
|
1145
|
+
fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
|
|
611
1146
|
})),
|
|
612
1147
|
deriving: ["Repr", "Inhabited"],
|
|
613
1148
|
};
|
|
614
1149
|
}
|
|
1150
|
+
else if (d.kind === "alias") {
|
|
1151
|
+
return {
|
|
1152
|
+
kind: "type-alias", name: d.name,
|
|
1153
|
+
target: parseTsType(d.aliasOf),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
615
1156
|
else {
|
|
616
1157
|
return {
|
|
617
1158
|
kind: "structure", name: d.name,
|
|
618
|
-
fields: d.fields.map(f => ({ name: f.name, type:
|
|
1159
|
+
fields: d.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
|
|
619
1160
|
deriving: ["Repr", "Inhabited", "DecidableEq"],
|
|
620
1161
|
};
|
|
621
1162
|
}
|
|
622
1163
|
}
|
|
623
1164
|
// ── Helpers ──────────────────────────────────────────────────
|
|
1165
|
+
/** Find parameter names that are reassigned anywhere in the body. */
|
|
1166
|
+
function findReassignedNames(stmts, names) {
|
|
1167
|
+
const found = new Set();
|
|
1168
|
+
function scan(stmts) {
|
|
1169
|
+
for (const s of stmts) {
|
|
1170
|
+
if (s.kind === "assign" && names.has(s.target))
|
|
1171
|
+
found.add(s.target);
|
|
1172
|
+
if (s.kind === "ghostAssign" && names.has(s.target))
|
|
1173
|
+
found.add(s.target);
|
|
1174
|
+
// Mutating collection calls: s.add(x), m.set(k,v), s.delete(x), arr.push(x)
|
|
1175
|
+
if (s.kind === "expr" && s.expr.kind === "call" && s.expr.fn.kind === "field" &&
|
|
1176
|
+
s.expr.fn.obj.kind === "var" && names.has(s.expr.fn.obj.name) &&
|
|
1177
|
+
["add", "set", "delete", "push"].includes(s.expr.fn.field)) {
|
|
1178
|
+
found.add(s.expr.fn.obj.name);
|
|
1179
|
+
}
|
|
1180
|
+
if (s.kind === "if") {
|
|
1181
|
+
scan(s.then);
|
|
1182
|
+
scan(s.else);
|
|
1183
|
+
}
|
|
1184
|
+
if (s.kind === "while")
|
|
1185
|
+
scan(s.body);
|
|
1186
|
+
if (s.kind === "forof")
|
|
1187
|
+
scan(s.body);
|
|
1188
|
+
if (s.kind === "switch") {
|
|
1189
|
+
for (const c of s.cases)
|
|
1190
|
+
scan(c.body);
|
|
1191
|
+
scan(s.defaultBody);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
scan(stmts);
|
|
1196
|
+
return found;
|
|
1197
|
+
}
|
|
624
1198
|
/** Replace all occurrences of a variable name with a new expression. */
|
|
625
1199
|
function replaceVar(e, name, replacement) {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
case "toNat": return { ...e, expr: r(e.expr) };
|
|
639
|
-
case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
|
|
640
|
-
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(f => ({ ...f, value: r(f.value) })) };
|
|
641
|
-
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
642
|
-
case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
|
|
643
|
-
case "match": return { ...e, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
|
|
644
|
-
case "forall": return { ...e, body: e.var === name ? e : { ...e, body: r(e.body) } };
|
|
645
|
-
case "exists": return { ...e, body: e.var === name ? e : { ...e, body: r(e.body) } };
|
|
646
|
-
case "let": return { ...e, value: r(e.value), body: e.name === name ? e : { ...e, body: r(e.body) } };
|
|
647
|
-
case "dotCall": return { ...e, obj: r(e.obj), args: e.args.map(r) };
|
|
648
|
-
case "lambda": return e; // don't descend into lambdas
|
|
649
|
-
}
|
|
1200
|
+
return mapExpr(e, x => {
|
|
1201
|
+
if (x.kind === "var" && x.name === name)
|
|
1202
|
+
return replacement;
|
|
1203
|
+
// Don't descend past bindings that shadow the name
|
|
1204
|
+
if (x.kind === "forall" && x.var === name)
|
|
1205
|
+
return x;
|
|
1206
|
+
if (x.kind === "exists" && x.var === name)
|
|
1207
|
+
return x;
|
|
1208
|
+
if (x.kind === "let" && x.name === name)
|
|
1209
|
+
return { ...x, value: replaceVar(x.value, name, replacement) };
|
|
1210
|
+
return null;
|
|
1211
|
+
});
|
|
650
1212
|
}
|
|
651
1213
|
// ── Top-level transform ──────────────────────────────────────
|
|
1214
|
+
/** Transform for Lean backend — same logic, Lean options. */
|
|
1215
|
+
export function transformModuleLean(mod, specImport) {
|
|
1216
|
+
const prev = _opts;
|
|
1217
|
+
_opts = LEAN_OPTIONS;
|
|
1218
|
+
try {
|
|
1219
|
+
return transformModule(mod, specImport);
|
|
1220
|
+
}
|
|
1221
|
+
finally {
|
|
1222
|
+
_opts = prev;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
652
1225
|
/** Transform for Dafny backend — same logic, Dafny options. */
|
|
653
1226
|
export function transformModuleDafny(mod) {
|
|
654
1227
|
const prev = _opts;
|
|
@@ -661,7 +1234,16 @@ export function transformModuleDafny(mod) {
|
|
|
661
1234
|
}
|
|
662
1235
|
}
|
|
663
1236
|
export function transformModule(mod, specImport) {
|
|
1237
|
+
_forofCounters.clear();
|
|
1238
|
+
_typeDecls = mod.typeDecls;
|
|
664
1239
|
const typeDecls = mod.typeDecls.map(transformTypeDecl);
|
|
1240
|
+
// Module-level constants
|
|
1241
|
+
const constDecls = (mod.constants ?? []).map(c => ({
|
|
1242
|
+
kind: "const",
|
|
1243
|
+
name: c.name,
|
|
1244
|
+
type: c.ty,
|
|
1245
|
+
value: transformExpr(c.value),
|
|
1246
|
+
}));
|
|
665
1247
|
// Pure function mirrors
|
|
666
1248
|
const pureDefs = [];
|
|
667
1249
|
for (const fn of mod.functions) {
|
|
@@ -676,8 +1258,9 @@ export function transformModule(mod, specImport) {
|
|
|
676
1258
|
pureDefs.push({
|
|
677
1259
|
kind: "def",
|
|
678
1260
|
name: fn.name,
|
|
679
|
-
|
|
680
|
-
|
|
1261
|
+
typeParams: fn.typeParams,
|
|
1262
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1263
|
+
returnType: fn.returnTy,
|
|
681
1264
|
requires: fn.requires.map(transformExpr),
|
|
682
1265
|
ensures,
|
|
683
1266
|
body,
|
|
@@ -686,9 +1269,6 @@ export function transformModule(mod, specImport) {
|
|
|
686
1269
|
const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
|
|
687
1270
|
// Types file
|
|
688
1271
|
const typesImports = ["LemmaScript"];
|
|
689
|
-
for (const m of usedImports)
|
|
690
|
-
typesImports.push(MODULE_IMPORTS[m] ?? m);
|
|
691
|
-
usedImports.clear();
|
|
692
1272
|
let typesFile = null;
|
|
693
1273
|
const pureNamespace = pureDefs.length > 0
|
|
694
1274
|
? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
|
|
@@ -713,25 +1293,56 @@ export function transformModule(mod, specImport) {
|
|
|
713
1293
|
else
|
|
714
1294
|
ensures.push(transformExpr(e));
|
|
715
1295
|
}
|
|
716
|
-
|
|
1296
|
+
_forofCounters.clear();
|
|
1297
|
+
let body = pureDefNames.has(fn.name)
|
|
717
1298
|
? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
|
|
718
1299
|
: transformStmts(fn.body, mod.typeDecls);
|
|
1300
|
+
// Shadow reassigned parameters with mutable locals
|
|
1301
|
+
const paramNames = new Set(fn.params.map(p => p.name));
|
|
1302
|
+
const reassigned = findReassignedNames(fn.body, paramNames);
|
|
1303
|
+
if (reassigned.size > 0) {
|
|
1304
|
+
const shadows = fn.params
|
|
1305
|
+
.filter(p => reassigned.has(p.name))
|
|
1306
|
+
.map(p => ({ kind: "let", name: p.name, type: p.ty, mutable: true, value: { kind: "var", name: p.name } }));
|
|
1307
|
+
body = [...shadows, ...body];
|
|
1308
|
+
}
|
|
719
1309
|
return {
|
|
720
1310
|
kind: "method",
|
|
721
1311
|
name: fn.name,
|
|
722
|
-
|
|
723
|
-
|
|
1312
|
+
typeParams: fn.typeParams,
|
|
1313
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1314
|
+
returnType: fn.returnTy,
|
|
724
1315
|
requires: fn.requires.map(transformExpr),
|
|
725
1316
|
ensures,
|
|
726
1317
|
body,
|
|
727
1318
|
};
|
|
728
1319
|
});
|
|
1320
|
+
// Class declarations
|
|
1321
|
+
const classDecls = (mod.classes ?? []).map(cls => {
|
|
1322
|
+
const classMethods = cls.methods.map(fn => {
|
|
1323
|
+
const ensures = fn.ensures.map(transformExpr);
|
|
1324
|
+
_forofCounters.clear();
|
|
1325
|
+
const body = transformStmts(fn.body, mod.typeDecls);
|
|
1326
|
+
return {
|
|
1327
|
+
kind: "method",
|
|
1328
|
+
name: fn.name,
|
|
1329
|
+
typeParams: fn.typeParams,
|
|
1330
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1331
|
+
returnType: fn.returnTy,
|
|
1332
|
+
requires: fn.requires.map(transformExpr),
|
|
1333
|
+
ensures,
|
|
1334
|
+
body,
|
|
1335
|
+
};
|
|
1336
|
+
});
|
|
1337
|
+
return {
|
|
1338
|
+
kind: "class",
|
|
1339
|
+
name: cls.name,
|
|
1340
|
+
fields: cls.fields.map(f => ({ name: f.name, type: f.ty })),
|
|
1341
|
+
methods: classMethods,
|
|
1342
|
+
};
|
|
1343
|
+
});
|
|
729
1344
|
const defImport = specImport ?? (typesFile ? `«${base}.types»` : null);
|
|
730
1345
|
const defBaseImports = defImport ? [defImport] : ["LemmaScript"];
|
|
731
|
-
if (!typesFile)
|
|
732
|
-
for (const m of usedImports)
|
|
733
|
-
defBaseImports.push(MODULE_IMPORTS[m] ?? m);
|
|
734
|
-
usedImports.clear();
|
|
735
1346
|
const defFile = {
|
|
736
1347
|
comment: " Generated by lsc from " + (mod.file.split("/").pop() ?? "") + "\n Do not edit — re-run `lsc gen` to regenerate.",
|
|
737
1348
|
imports: defBaseImports,
|
|
@@ -739,7 +1350,7 @@ export function transformModule(mod, specImport) {
|
|
|
739
1350
|
{ key: "loom.semantics.termination", value: '"total"' },
|
|
740
1351
|
{ key: "loom.semantics.choice", value: '"demonic"' },
|
|
741
1352
|
],
|
|
742
|
-
decls: methods,
|
|
1353
|
+
decls: [...constDecls, ...methods, ...classDecls],
|
|
743
1354
|
};
|
|
744
1355
|
return { typesFile, defFile };
|
|
745
1356
|
}
|