lemmascript 0.1.0 → 0.2.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 +24 -31
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +46 -52
- package/tools/dist/dafny-emit.js +362 -73
- package/tools/dist/extract.js +386 -15
- package/tools/dist/ir.js +2 -2
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +393 -0
- package/tools/dist/lsc.js +44 -43
- package/tools/dist/resolve.js +285 -19
- package/tools/dist/specparser.js +61 -7
- package/tools/dist/transform.js +533 -187
- package/tools/dist/types.js +46 -13
package/tools/dist/transform.js
CHANGED
|
@@ -1,56 +1,78 @@
|
|
|
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": return { ...e, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
|
|
39
|
+
case "forall": return { ...e, body: r(e.body) };
|
|
40
|
+
case "exists": return { ...e, body: r(e.body) };
|
|
41
|
+
case "let": return { ...e, value: r(e.value), body: r(e.body) };
|
|
42
|
+
case "methodCall": return { ...e, obj: r(e.obj), args: e.args.map(r) };
|
|
43
|
+
case "lambda": return e;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Map over all expressions in a statement tree. */
|
|
47
|
+
function mapStmt(s, f) {
|
|
48
|
+
const r = (e) => mapExpr(e, f);
|
|
49
|
+
switch (s.kind) {
|
|
50
|
+
case "let": return { ...s, value: r(s.value) };
|
|
51
|
+
case "assign": return { ...s, value: r(s.value) };
|
|
52
|
+
case "bind": return { ...s, value: r(s.value) };
|
|
53
|
+
case "let-bind": return { ...s, value: r(s.value) };
|
|
54
|
+
case "return": return { ...s, value: r(s.value) };
|
|
55
|
+
case "break":
|
|
56
|
+
case "continue": return s;
|
|
57
|
+
case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapStmt(t, f)), else: s.else.map(t => mapStmt(t, f)) };
|
|
58
|
+
case "match": return { ...s, arms: s.arms.map(a => ({ ...a, body: a.body.map(t => mapStmt(t, f)) })) };
|
|
59
|
+
case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
|
|
60
|
+
case "forin": return { ...s, bound: r(s.bound), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
|
|
61
|
+
case "ghostLet": return { ...s, value: r(s.value) };
|
|
62
|
+
case "ghostAssign": return { ...s, value: r(s.value) };
|
|
63
|
+
case "assert": return { ...s, expr: r(s.expr) };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function mapStmts(stmts, f) {
|
|
67
|
+
return stmts.map(s => mapStmt(s, f));
|
|
68
|
+
}
|
|
8
69
|
export const LEAN_OPTIONS = {
|
|
9
70
|
backend: "lean",
|
|
10
71
|
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
72
|
};
|
|
32
73
|
export const DAFNY_OPTIONS = {
|
|
33
74
|
backend: "dafny",
|
|
34
75
|
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
76
|
};
|
|
55
77
|
/** Active options — set before each transform call. */
|
|
56
78
|
let _opts = LEAN_OPTIONS;
|
|
@@ -58,13 +80,10 @@ let _opts = LEAN_OPTIONS;
|
|
|
58
80
|
function matchBinder(fieldName) {
|
|
59
81
|
return `_${fieldName}`;
|
|
60
82
|
}
|
|
83
|
+
const _forofCounters = new Map();
|
|
61
84
|
function isNat(ty) { return ty.kind === "nat"; }
|
|
62
85
|
function isArray(ty) { return ty.kind === "array"; }
|
|
63
86
|
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
87
|
/** Check if transformed lambda body contains monadic binds. */
|
|
69
88
|
function isMonadicBody(stmts) {
|
|
70
89
|
for (const s of stmts) {
|
|
@@ -84,32 +103,20 @@ function isMonadicBody(stmts) {
|
|
|
84
103
|
}
|
|
85
104
|
return false;
|
|
86
105
|
}
|
|
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
106
|
// ── Transform expressions ────────────────────────────────────
|
|
107
|
+
/** Prop-valued operators (for specs/invariants). */
|
|
105
108
|
const OP_MAP = {
|
|
106
109
|
"===": "=", "!==": "≠", ">=": "≥", "<=": "≤", ">": ">", "<": "<",
|
|
107
110
|
"&&": "∧", "||": "∨", "+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
|
|
108
111
|
"==": "=", "!=": "≠",
|
|
109
112
|
};
|
|
113
|
+
/** Bool-valued operators (for code-level conditions needing Decidable). */
|
|
114
|
+
const BOOL_OP_MAP = {
|
|
115
|
+
...OP_MAP, "===": "==", "!==": "!=",
|
|
116
|
+
};
|
|
110
117
|
function transformExpr(e) { return lowerExpr(e, null); }
|
|
111
118
|
/**
|
|
112
|
-
* Lower a typed expression to
|
|
119
|
+
* Lower a typed expression to Backend IR.
|
|
113
120
|
*
|
|
114
121
|
* When `binds` is non-null, embedded method calls are extracted into
|
|
115
122
|
* `let ← ` binds (monadic lifting / selective ANF). Lifting propagates
|
|
@@ -139,6 +146,20 @@ function lowerExpr(e, binds) {
|
|
|
139
146
|
case "unop":
|
|
140
147
|
if (e.op === "-" && e.expr.kind === "num")
|
|
141
148
|
return { kind: "num", value: -e.expr.value };
|
|
149
|
+
// String truthiness: !str → str == ""
|
|
150
|
+
if (e.op === "!" && e.expr.ty.kind === "string")
|
|
151
|
+
return { kind: "binop", op: "=", left: lowerExpr(e.expr, binds), right: { kind: "str", value: "" } };
|
|
152
|
+
// Optional truthiness: !opt → opt is None
|
|
153
|
+
if (e.op === "!" && e.expr.ty.kind === "optional") {
|
|
154
|
+
const bound = matchBinder("value");
|
|
155
|
+
return {
|
|
156
|
+
kind: "match", scrutinee: lowerExpr(e.expr, binds),
|
|
157
|
+
arms: [
|
|
158
|
+
{ pattern: `.some ${bound}`, body: { kind: "bool", value: false } },
|
|
159
|
+
{ pattern: ".none", body: { kind: "bool", value: true } },
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
142
163
|
return { kind: "unop", op: e.op === "!" ? "¬" : e.op, expr: lowerExpr(e.expr, binds) };
|
|
143
164
|
case "binop": {
|
|
144
165
|
// Implication: flatten (A && B) ==> C → implies [A, B] C
|
|
@@ -166,6 +187,47 @@ function lowerExpr(e, binds) {
|
|
|
166
187
|
: { kind: "str", value: e.right.value };
|
|
167
188
|
return { kind: "binop", op: e.op === "===" ? "=" : "≠", left, right };
|
|
168
189
|
}
|
|
190
|
+
// Optional comparison: optExpr op val → match optExpr { Some(v) => v op val, None => false/true }
|
|
191
|
+
if (["===", "!==", ">=", "<=", ">", "<"].includes(e.op) &&
|
|
192
|
+
(e.left.ty.kind === "optional") !== (e.right.ty.kind === "optional")) {
|
|
193
|
+
const [optSide, valSide] = e.left.ty.kind === "optional" ? [e.left, e.right] : [e.right, e.left];
|
|
194
|
+
const optExpr = lowerExpr(optSide, binds);
|
|
195
|
+
// x === undefined → None?, x !== undefined → Some?
|
|
196
|
+
if (valSide.kind === "var" && valSide.name === "undefined") {
|
|
197
|
+
const isNone = e.op === "===";
|
|
198
|
+
return {
|
|
199
|
+
kind: "match", scrutinee: optExpr,
|
|
200
|
+
arms: [
|
|
201
|
+
{ pattern: ".some _", body: { kind: "bool", value: !isNone } },
|
|
202
|
+
{ pattern: ".none", body: { kind: "bool", value: isNone } },
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const valExpr = lowerExpr(valSide, binds);
|
|
207
|
+
const cmpOp = BOOL_OP_MAP[e.op] ?? e.op;
|
|
208
|
+
const noneVal = e.op === "!==" ? true : false;
|
|
209
|
+
const bound = matchBinder("value");
|
|
210
|
+
return {
|
|
211
|
+
kind: "match", scrutinee: optExpr,
|
|
212
|
+
arms: [
|
|
213
|
+
{ pattern: `.some ${bound}`, body: { kind: "binop", op: cmpOp, left: { kind: "var", name: bound }, right: valExpr } },
|
|
214
|
+
{ pattern: ".none", body: { kind: "bool", value: noneVal } },
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// || on optional → match Some/None with default
|
|
219
|
+
if (e.op === "||" && e.left.ty.kind === "optional") {
|
|
220
|
+
const optExpr = lowerExpr(e.left, binds);
|
|
221
|
+
const defaultExpr = lowerExpr(e.right, binds);
|
|
222
|
+
const bound = matchBinder("value");
|
|
223
|
+
return {
|
|
224
|
+
kind: "match", scrutinee: optExpr,
|
|
225
|
+
arms: [
|
|
226
|
+
{ pattern: `.some ${bound}`, body: { kind: "var", name: bound } },
|
|
227
|
+
{ pattern: ".none", body: defaultExpr },
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
169
231
|
return {
|
|
170
232
|
kind: "binop",
|
|
171
233
|
op: OP_MAP[e.op] ?? e.op,
|
|
@@ -178,6 +240,8 @@ function lowerExpr(e, binds) {
|
|
|
178
240
|
return { kind: "field", obj: transformExpr(e.obj), field: "size" };
|
|
179
241
|
if (e.field === "length" && e.obj.ty.kind === "string")
|
|
180
242
|
return { kind: "field", obj: transformExpr(e.obj), field: "length" };
|
|
243
|
+
if (e.field === "size" && (e.obj.ty.kind === "map" || e.obj.ty.kind === "set"))
|
|
244
|
+
return { kind: "field", obj: transformExpr(e.obj), field: "collectionSize" };
|
|
181
245
|
return { kind: "field", obj: transformExpr(e.obj), field: e.field };
|
|
182
246
|
case "index": {
|
|
183
247
|
const idx = transformExpr(e.idx);
|
|
@@ -185,43 +249,47 @@ function lowerExpr(e, binds) {
|
|
|
185
249
|
return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
|
|
186
250
|
}
|
|
187
251
|
case "call": {
|
|
188
|
-
// Math.
|
|
252
|
+
// Math.ceil(x): CeilReal on real args, identity on int
|
|
253
|
+
if (e.fn.kind === "field" && e.fn.field === "ceil" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
|
|
254
|
+
const arg = e.args[0];
|
|
255
|
+
if (arg.ty.kind === "real")
|
|
256
|
+
return { kind: "app", fn: "CeilReal", args: [lowerExpr(arg, binds)] };
|
|
257
|
+
return lowerExpr(arg, binds);
|
|
258
|
+
}
|
|
259
|
+
// Math.floor(x): FloorReal on real args, JSFloorDiv for int division, identity on int
|
|
189
260
|
if (e.fn.kind === "field" && e.fn.field === "floor" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
|
|
190
261
|
const arg = e.args[0];
|
|
262
|
+
if (arg.ty.kind === "real")
|
|
263
|
+
return { kind: "app", fn: "FloorReal", args: [lowerExpr(arg, binds)] };
|
|
191
264
|
if (_opts.backend === "dafny" && arg.kind === "binop" && arg.op === "/")
|
|
192
265
|
return { kind: "app", fn: "JSFloorDiv", args: [lowerExpr(arg.left, binds), lowerExpr(arg.right, binds)] };
|
|
193
266
|
return lowerExpr(arg, binds);
|
|
194
267
|
}
|
|
195
|
-
//
|
|
268
|
+
// Method call: receiver.method(args) → methodCall node
|
|
196
269
|
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;
|
|
270
|
+
const recv = lowerExpr(e.fn.obj, binds);
|
|
271
|
+
let method = e.fn.field;
|
|
272
|
+
const args = e.args.map((a, i) => {
|
|
273
|
+
const lowered = lowerExpr(a, binds);
|
|
274
|
+
// arr.with index (first arg) needs .toNat when Int-typed
|
|
275
|
+
if (e.fn.kind === "field" && e.fn.field === "with" && e.fn.obj.ty.kind === "array" && i === 0 && !isNat(a.ty))
|
|
276
|
+
return { kind: "toNat", expr: lowered };
|
|
277
|
+
return lowered;
|
|
278
|
+
});
|
|
279
|
+
// Spec-context map get: result type is non-optional → direct access
|
|
280
|
+
if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
|
|
281
|
+
method = "getDirect";
|
|
282
|
+
}
|
|
283
|
+
// Check if any lambda arg has monadic body
|
|
284
|
+
const needsMonadic = _opts.monadic && args.some(a => a.kind === "lambda" && isMonadicBody(a.body));
|
|
285
|
+
const result = { kind: "methodCall", obj: recv, objTy: e.fn.obj.ty, method, args, monadic: needsMonadic };
|
|
286
|
+
// Monadic HOF call is itself monadic — lift via binds like a method call
|
|
287
|
+
if (_opts.monadic && needsMonadic && binds) {
|
|
288
|
+
const name = `_t${_liftCounter++}`;
|
|
289
|
+
binds.push({ kind: "let-bind", name, value: result });
|
|
290
|
+
return { kind: "var", name };
|
|
223
291
|
}
|
|
224
|
-
|
|
292
|
+
return result;
|
|
225
293
|
}
|
|
226
294
|
if (e.fn.kind !== "var")
|
|
227
295
|
throw new Error(`Unsupported call expression: ${e.fn.kind}`);
|
|
@@ -231,15 +299,46 @@ function lowerExpr(e, binds) {
|
|
|
231
299
|
case "record":
|
|
232
300
|
return { kind: "record", spread: e.spread ? lowerExpr(e.spread, binds) : null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
|
|
233
301
|
case "arrayLiteral":
|
|
302
|
+
if (e.ty.kind === "map" && e.elems.length === 0)
|
|
303
|
+
return { kind: "emptyMap" };
|
|
304
|
+
if (e.ty.kind === "set" && e.elems.length === 0)
|
|
305
|
+
return { kind: "emptySet" };
|
|
234
306
|
return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
|
|
235
307
|
case "lambda":
|
|
236
|
-
return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type:
|
|
308
|
+
return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: p.ty })), body: transformStmts(e.body, []) };
|
|
237
309
|
case "forall":
|
|
238
|
-
return { kind: "forall", var: e.var, type:
|
|
310
|
+
return { kind: "forall", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
239
311
|
case "exists":
|
|
240
|
-
return { kind: "exists", var: e.var, type:
|
|
241
|
-
case "conditional":
|
|
242
|
-
|
|
312
|
+
return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
313
|
+
case "conditional": {
|
|
314
|
+
const cond = lowerExpr(e.cond, binds);
|
|
315
|
+
let thenExpr = lowerExpr(e.then, binds);
|
|
316
|
+
let elseExpr = lowerExpr(e.else, binds);
|
|
317
|
+
// Optional ternary: wrap non-undefined branch in Some, undefined branch in None
|
|
318
|
+
if (e.ty.kind === "optional") {
|
|
319
|
+
if (e.then.kind === "var" && e.then.name === "undefined") {
|
|
320
|
+
thenExpr = { kind: "constructor", name: ".none" };
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
thenExpr = { kind: "app", fn: "Some", args: [thenExpr] };
|
|
324
|
+
}
|
|
325
|
+
if (e.else.kind === "var" && e.else.name === "undefined") {
|
|
326
|
+
elseExpr = { kind: "constructor", name: ".none" };
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
elseExpr = { kind: "app", fn: "Some", args: [elseExpr] };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { kind: "if", cond, then: thenExpr, else: elseExpr };
|
|
333
|
+
}
|
|
334
|
+
case "havoc":
|
|
335
|
+
// Dafny's * only works in var/assign positions — lift to own declaration
|
|
336
|
+
if (binds) {
|
|
337
|
+
const name = `_t${_liftCounter++}`;
|
|
338
|
+
binds.push({ kind: "let", name, type: e.ty, mutable: false, value: { kind: "havoc", type: e.ty } });
|
|
339
|
+
return { kind: "var", name };
|
|
340
|
+
}
|
|
341
|
+
return { kind: "havoc", type: e.ty };
|
|
243
342
|
}
|
|
244
343
|
}
|
|
245
344
|
function flattenImpl(e) {
|
|
@@ -281,31 +380,17 @@ function ensuresToMatch(e, typeDecls) {
|
|
|
281
380
|
return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
|
|
282
381
|
}
|
|
283
382
|
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
|
-
}
|
|
383
|
+
return mapExpr(e, x => {
|
|
384
|
+
if (x.kind === "field" && x.obj.kind === "var" && x.obj.name === varName) {
|
|
385
|
+
const f = fields.find(f => f.name === x.field);
|
|
386
|
+
if (f)
|
|
387
|
+
return { kind: "var", name: matchBinder(f.name) };
|
|
388
|
+
}
|
|
389
|
+
// If this let shadows the matched variable, stop replacing in the body
|
|
390
|
+
if (x.kind === "let" && x.name === varName)
|
|
391
|
+
return { ...x, value: replaceFieldAccess(x.value, varName, fields) };
|
|
392
|
+
return null;
|
|
393
|
+
});
|
|
309
394
|
}
|
|
310
395
|
// ── Transform statements ─────────────────────────────────────
|
|
311
396
|
function transformStmts(stmts, typeDecls) {
|
|
@@ -321,20 +406,76 @@ function transformStmts(stmts, typeDecls) {
|
|
|
321
406
|
i += chain.consumed;
|
|
322
407
|
continue;
|
|
323
408
|
}
|
|
409
|
+
// Detect optional check → match on Some/None
|
|
410
|
+
const opt = parseOptionalCheck(s.cond);
|
|
411
|
+
if (opt) {
|
|
412
|
+
const rest = stmts.slice(i + 1);
|
|
413
|
+
result.push(emitOptionalMatch(opt.varName, opt.negated, s, typeDecls, rest));
|
|
414
|
+
// If rest was consumed into the Some branch, skip remaining
|
|
415
|
+
const someBranch = opt.negated ? s.else : s.then;
|
|
416
|
+
if (someBranch.length === 0 && rest.length > 0) {
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
i++;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
324
422
|
}
|
|
325
423
|
// Transform for-of → for-in over range
|
|
326
424
|
if (s.kind === "forof") {
|
|
327
|
-
const
|
|
328
|
-
const
|
|
425
|
+
const varName = s.names[0];
|
|
426
|
+
const varTy = s.nameTypes[0] ?? { kind: "unknown" };
|
|
427
|
+
let iterExpr = transformExpr(s.iterable);
|
|
428
|
+
// Map iteration: for (const [k, v] of map) → iterate keys, look up values
|
|
429
|
+
if (s.names.length >= 2 && s.iterable.ty.kind === "map") {
|
|
430
|
+
const keyName = s.names[0], valueName = s.names[1];
|
|
431
|
+
const keyTy = s.nameTypes[0] ?? { kind: "unknown" };
|
|
432
|
+
const valueTy = s.nameTypes[1] ?? { kind: "unknown" };
|
|
433
|
+
const keysSeqName = `_${keyName}_keys`;
|
|
434
|
+
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
435
|
+
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
436
|
+
const keysVar = { kind: "var", name: keysSeqName };
|
|
437
|
+
const count = _forofCounters.get(keyName) ?? 0;
|
|
438
|
+
_forofCounters.set(keyName, count + 1);
|
|
439
|
+
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
440
|
+
const idxName = `_${keyName}_idx${suffix}`;
|
|
441
|
+
const idx = { kind: "var", name: idxName };
|
|
442
|
+
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
443
|
+
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
444
|
+
const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
|
|
445
|
+
const letVal = { kind: "let", name: valueName, type: valueTy, mutable: false,
|
|
446
|
+
value: { kind: "methodCall", obj: iterExpr, objTy: s.iterable.ty, method: "getDirect", args: [{ kind: "var", name: keyName }], monadic: false } };
|
|
447
|
+
const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
|
|
448
|
+
result.push({
|
|
449
|
+
kind: "forin", idx: idxName, bound: arrSize,
|
|
450
|
+
invariants: [boundInv, ...s.invariants.map(transformExpr)],
|
|
451
|
+
body: [letKey, letVal, ...bodyStmts],
|
|
452
|
+
});
|
|
453
|
+
i++;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
// Sets aren't indexable — bind SetToSeq to a variable for iteration
|
|
457
|
+
if (s.iterable.ty.kind === "set") {
|
|
458
|
+
const seqName = `_${varName}_seq`;
|
|
459
|
+
const convExpr = { kind: "app", fn: "SetToSeq", args: [iterExpr] };
|
|
460
|
+
const elemTy = varTy.kind !== "unknown" ? varTy : { kind: "string" };
|
|
461
|
+
result.push({ kind: "let", name: seqName, type: { kind: "array", elem: elemTy }, mutable: false, value: convExpr });
|
|
462
|
+
iterExpr = { kind: "var", name: seqName };
|
|
463
|
+
}
|
|
464
|
+
const count = _forofCounters.get(varName) ?? 0;
|
|
465
|
+
_forofCounters.set(varName, count + 1);
|
|
466
|
+
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
467
|
+
const idxName = `_${varName}_idx${suffix}`;
|
|
329
468
|
const idx = { kind: "var", name: idxName };
|
|
330
|
-
const arrSize = { kind: "field", obj:
|
|
469
|
+
const arrSize = { kind: "field", obj: iterExpr, field: "size" };
|
|
331
470
|
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
332
|
-
const letElem = { kind: "let", name:
|
|
471
|
+
const letElem = { kind: "let", name: varName, type: varTy, mutable: false, value: { kind: "index", arr: iterExpr, idx } };
|
|
472
|
+
// Auto-add bound invariant: idx ≤ bound (always true for range loops)
|
|
473
|
+
const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
|
|
333
474
|
result.push({
|
|
334
475
|
kind: "forin",
|
|
335
476
|
idx: idxName,
|
|
336
477
|
bound: arrSize,
|
|
337
|
-
invariants: s.invariants.map(transformExpr),
|
|
478
|
+
invariants: [boundInv, ...s.invariants.map(transformExpr)],
|
|
338
479
|
body: [letElem, ...bodyStmts],
|
|
339
480
|
});
|
|
340
481
|
i++;
|
|
@@ -354,8 +495,71 @@ function liftMethodCalls(e) {
|
|
|
354
495
|
function transformStmt(s, typeDecls) {
|
|
355
496
|
switch (s.kind) {
|
|
356
497
|
case "let": {
|
|
498
|
+
// Whole-init havoc: emit directly, no lifting
|
|
499
|
+
if (s.init.kind === "havoc") {
|
|
500
|
+
return [{ kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: { kind: "havoc", type: s.init.ty } }];
|
|
501
|
+
}
|
|
502
|
+
// arr.shift()! → let x = arr[0]; arr = arr[1..]
|
|
503
|
+
const init = s.init.kind === "call" ? s.init : undefined;
|
|
504
|
+
if (init && init.fn.kind === "field" && init.fn.field === "shift" && init.fn.obj.ty.kind === "array") {
|
|
505
|
+
const arrName = init.fn.obj.kind === "var" ? init.fn.obj.name : undefined;
|
|
506
|
+
if (arrName) {
|
|
507
|
+
const arrVar = { kind: "var", name: arrName };
|
|
508
|
+
const letHead = { kind: "let", name: s.name, type: s.ty, mutable: s.mutable,
|
|
509
|
+
value: { kind: "index", arr: arrVar, idx: { kind: "num", value: 0 } } };
|
|
510
|
+
const sliceTail = { kind: "assign", target: arrName,
|
|
511
|
+
value: { kind: "methodCall", obj: arrVar, objTy: init.fn.obj.ty, method: "slice", args: [{ kind: "num", value: 1 }], monadic: false } };
|
|
512
|
+
return [letHead, sliceTail];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// new Map(arr.map(n => [n.field, n])) → let m = map[]; for (n of arr) m[n.field] := n
|
|
516
|
+
if (init && init.fn.kind === "var" && init.fn.name === "__mapFromArray" &&
|
|
517
|
+
init.args.length === 1 && init.args[0].kind === "call" &&
|
|
518
|
+
init.args[0].fn.kind === "field" && init.args[0].fn.field === "map" &&
|
|
519
|
+
init.args[0].args.length === 1 && init.args[0].args[0].kind === "lambda") {
|
|
520
|
+
const arrExpr = init.args[0].fn.obj;
|
|
521
|
+
const lam = init.args[0].args[0];
|
|
522
|
+
const param = lam.params[0]?.name ?? "_";
|
|
523
|
+
const lamBody = Array.isArray(lam.body) ? lam.body : [{ kind: "return", value: lam.body }];
|
|
524
|
+
const retStmt = lamBody.find(b => b.kind === "return");
|
|
525
|
+
if (retStmt && retStmt.value.kind === "arrayLiteral" && retStmt.value.elems.length === 2) {
|
|
526
|
+
const keyExpr = retStmt.value.elems[0];
|
|
527
|
+
const valExpr = retStmt.value.elems[1];
|
|
528
|
+
const arrIR = transformExpr(arrExpr);
|
|
529
|
+
const arrTy = arrExpr.ty;
|
|
530
|
+
const elemTy = arrTy.kind === "array" ? arrTy.elem : { kind: "unknown" };
|
|
531
|
+
const idxName = `_${param}_idx`;
|
|
532
|
+
const idx = { kind: "var", name: idxName };
|
|
533
|
+
const arrSize = { kind: "field", obj: arrIR, field: "size" };
|
|
534
|
+
const elemVar = { kind: "var", name: param };
|
|
535
|
+
const keyIR = transformExpr(keyExpr);
|
|
536
|
+
const valIR = transformExpr(valExpr);
|
|
537
|
+
const mapSet = { kind: "methodCall", obj: { kind: "var", name: s.name }, objTy: s.ty, method: "set", args: [keyIR, valIR], monadic: false };
|
|
538
|
+
// Auto-invariant: all processed elements' keys are in the map
|
|
539
|
+
const kVar = { kind: "var", name: "ki" };
|
|
540
|
+
const mapHasKey = {
|
|
541
|
+
kind: "implies",
|
|
542
|
+
premises: [
|
|
543
|
+
{ kind: "binop", op: "≥", left: kVar, right: { kind: "num", value: 0 } },
|
|
544
|
+
{ kind: "binop", op: "<", left: kVar, right: idx },
|
|
545
|
+
],
|
|
546
|
+
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 },
|
|
547
|
+
};
|
|
548
|
+
const autoInv = { kind: "forall", var: "ki", type: { kind: "int" }, body: mapHasKey };
|
|
549
|
+
const stmts = [
|
|
550
|
+
{ kind: "let", name: s.name, type: s.ty, mutable: true, value: { kind: "emptyMap" } },
|
|
551
|
+
{ kind: "forin", idx: idxName, bound: arrSize,
|
|
552
|
+
invariants: [{ kind: "binop", op: "≤", left: idx, right: arrSize }, autoInv],
|
|
553
|
+
body: [
|
|
554
|
+
{ kind: "let", name: param, type: elemTy, mutable: false, value: { kind: "index", arr: arrIR, idx } },
|
|
555
|
+
{ kind: "assign", target: s.name, value: mapSet },
|
|
556
|
+
] },
|
|
557
|
+
];
|
|
558
|
+
return stmts;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
357
561
|
const { binds, expr } = liftMethodCalls(s.init);
|
|
358
|
-
return [...binds, { kind: "let", name: s.name, type:
|
|
562
|
+
return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
|
|
359
563
|
}
|
|
360
564
|
case "assign": {
|
|
361
565
|
// Top-level method call → direct monadic bind, no lifting needed
|
|
@@ -371,6 +575,37 @@ function transformStmt(s, typeDecls) {
|
|
|
371
575
|
case "break": return [{ kind: "break" }];
|
|
372
576
|
case "continue": return [{ kind: "continue" }];
|
|
373
577
|
case "expr": {
|
|
578
|
+
// Mutating collection call: m.set(k, v) → m := m.set(k, v)
|
|
579
|
+
// Same for s.add(x) on sets, arr.push(x)
|
|
580
|
+
if (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
|
|
581
|
+
s.expr.fn.obj.kind === "var" &&
|
|
582
|
+
((s.expr.fn.obj.ty.kind === "map" || s.expr.fn.obj.ty.kind === "set") &&
|
|
583
|
+
(s.expr.fn.field === "set" || s.expr.fn.field === "add" || s.expr.fn.field === "delete")) ||
|
|
584
|
+
(s.expr.kind === "call" && s.expr.fn.kind === "field" &&
|
|
585
|
+
s.expr.fn.obj.kind === "var" && s.expr.fn.obj.ty.kind === "array" &&
|
|
586
|
+
s.expr.fn.field === "push")) {
|
|
587
|
+
const receiver = s.expr.fn.obj.name;
|
|
588
|
+
const { binds, expr } = liftMethodCalls(s.expr);
|
|
589
|
+
return [...binds, { kind: "assign", target: receiver, value: expr }];
|
|
590
|
+
}
|
|
591
|
+
// Optional chaining on map.get: m.get(k)?.push(v) → if k in m { m[k] := m[k] + [v] }
|
|
592
|
+
if (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
|
|
593
|
+
s.expr.fn.obj.kind === "call" && s.expr.fn.obj.fn.kind === "field" &&
|
|
594
|
+
s.expr.fn.obj.fn.obj.ty.kind === "map" && s.expr.fn.obj.fn.field === "get" &&
|
|
595
|
+
s.expr.fn.field === "push") {
|
|
596
|
+
const mapExpr = s.expr.fn.obj.fn.obj;
|
|
597
|
+
const mapName = mapExpr.kind === "var" ? mapExpr.name : undefined;
|
|
598
|
+
const keyExpr = lowerExpr(s.expr.fn.obj.args[0], null);
|
|
599
|
+
const pushArg = lowerExpr(s.expr.args[0], null);
|
|
600
|
+
if (mapName) {
|
|
601
|
+
const mapVar = { kind: "var", name: mapName };
|
|
602
|
+
const directGet = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "getDirect", args: [keyExpr], monadic: false };
|
|
603
|
+
const pushed = { kind: "methodCall", obj: directGet, objTy: mapExpr.ty.value, method: "push", args: [pushArg], monadic: false };
|
|
604
|
+
const updated = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "set", args: [keyExpr, pushed], monadic: false };
|
|
605
|
+
const hasCond = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "has", args: [keyExpr], monadic: false };
|
|
606
|
+
return [{ kind: "if", cond: hasCond, then: [{ kind: "assign", target: mapName, value: updated }], else: [] }];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
374
609
|
const { binds, expr } = liftMethodCalls(s.expr);
|
|
375
610
|
return [...binds, { kind: "assign", target: "_", value: expr }];
|
|
376
611
|
}
|
|
@@ -388,10 +623,18 @@ function transformStmt(s, typeDecls) {
|
|
|
388
623
|
doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
|
|
389
624
|
body: transformStmts(s.body, typeDecls),
|
|
390
625
|
}];
|
|
626
|
+
case "throw":
|
|
627
|
+
return [{ kind: "assert", expr: { kind: "bool", value: false } }];
|
|
391
628
|
case "forof":
|
|
392
629
|
throw new Error("forof should be transformed to forin (range loop) in transformStmts");
|
|
393
630
|
case "switch":
|
|
394
631
|
return [emitSwitchStmt(s, typeDecls)];
|
|
632
|
+
case "ghostLet":
|
|
633
|
+
return [{ kind: "ghostLet", name: s.name, type: s.ty, value: transformExpr(s.init) }];
|
|
634
|
+
case "ghostAssign":
|
|
635
|
+
return [{ kind: "ghostAssign", target: s.target, value: transformExpr(s.value) }];
|
|
636
|
+
case "assert":
|
|
637
|
+
return [{ kind: "assert", expr: transformExpr(s.expr) }];
|
|
395
638
|
}
|
|
396
639
|
}
|
|
397
640
|
function detectDiscriminantChain(stmts) {
|
|
@@ -443,6 +686,41 @@ function parseDiscriminantCond(cond) {
|
|
|
443
686
|
return null;
|
|
444
687
|
return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
445
688
|
}
|
|
689
|
+
function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
|
|
690
|
+
let someBranch = negated ? s.else : s.then;
|
|
691
|
+
const noneBranch = negated ? s.then : s.else;
|
|
692
|
+
// Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
|
|
693
|
+
// so include remaining statements as the Some body
|
|
694
|
+
if (someBranch.length === 0 && restStmts && restStmts.length > 0) {
|
|
695
|
+
someBranch = restStmts;
|
|
696
|
+
}
|
|
697
|
+
const bound = matchBinder(`${varName}_val`);
|
|
698
|
+
const someBody = transformStmts(someBranch, typeDecls);
|
|
699
|
+
const r = (e) => replaceVar(e, varName, { kind: "var", name: bound });
|
|
700
|
+
const someReplaced = someBody.map(stmt => mapStmtExprs(stmt, r));
|
|
701
|
+
const arms = [
|
|
702
|
+
{ pattern: `.some ${bound}`, body: someReplaced },
|
|
703
|
+
{ pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
|
|
704
|
+
];
|
|
705
|
+
return { kind: "match", scrutinee: varName, arms };
|
|
706
|
+
}
|
|
707
|
+
/** Apply an expression transform to all expressions in a statement (convenience wrapper). */
|
|
708
|
+
function mapStmtExprs(s, r) {
|
|
709
|
+
return mapStmt(s, e => r(e));
|
|
710
|
+
}
|
|
711
|
+
/** Detect `v !== undefined` or `undefined !== v` where v has optional type. */
|
|
712
|
+
function parseOptionalCheck(cond) {
|
|
713
|
+
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
|
|
714
|
+
return null;
|
|
715
|
+
let varExpr = null;
|
|
716
|
+
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
717
|
+
varExpr = cond.left;
|
|
718
|
+
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
719
|
+
varExpr = cond.right;
|
|
720
|
+
if (!varExpr || varExpr.kind !== "var" || varExpr.ty.kind !== "optional")
|
|
721
|
+
return null;
|
|
722
|
+
return { varName: varExpr.name, negated: cond.op === "===" };
|
|
723
|
+
}
|
|
446
724
|
function emitMatchStmt(chain, typeDecls) {
|
|
447
725
|
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
448
726
|
const arms = chain.cases.map(c => {
|
|
@@ -476,36 +754,26 @@ function emitSwitchStmt(s, typeDecls) {
|
|
|
476
754
|
function replaceFieldAccessInStmts(stmts, varName, fields) {
|
|
477
755
|
if (fields.length === 0)
|
|
478
756
|
return stmts;
|
|
757
|
+
const f = (e) => {
|
|
758
|
+
if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === varName) {
|
|
759
|
+
const fi = fields.find(fi => fi.name === e.field);
|
|
760
|
+
if (fi)
|
|
761
|
+
return { kind: "var", name: matchBinder(fi.name) };
|
|
762
|
+
}
|
|
763
|
+
return null;
|
|
764
|
+
};
|
|
479
765
|
const result = [];
|
|
480
766
|
for (const s of stmts) {
|
|
481
767
|
// If a let shadows the matched variable, stop replacing from here on
|
|
482
768
|
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
|
|
769
|
+
result.push(s.value ? { ...s, value: mapExpr(s.value, f) } : s);
|
|
486
770
|
result.push(...stmts.slice(result.length));
|
|
487
771
|
break;
|
|
488
772
|
}
|
|
489
|
-
result.push(
|
|
773
|
+
result.push(mapStmt(s, f));
|
|
490
774
|
}
|
|
491
775
|
return result;
|
|
492
776
|
}
|
|
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
777
|
// ── Pure function generation ─────────────────────────────────
|
|
510
778
|
function transformPureBody(stmts, typeDecls) {
|
|
511
779
|
// Detect discriminant if-chain
|
|
@@ -526,6 +794,29 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
526
794
|
return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
|
|
527
795
|
}
|
|
528
796
|
case "if": {
|
|
797
|
+
// Optional narrowing: if (x === undefined) → match x { None => ..., Some(x_val) => ... }
|
|
798
|
+
const optCheck = parseOptionalCheck(s.cond);
|
|
799
|
+
if (optCheck) {
|
|
800
|
+
let someBranch = optCheck.negated ? s.else : s.then;
|
|
801
|
+
const noneBranch = optCheck.negated ? s.then : (s.else.length > 0 ? s.else : rest);
|
|
802
|
+
if (someBranch.length === 0)
|
|
803
|
+
someBranch = rest;
|
|
804
|
+
const bound = matchBinder(`${optCheck.varName}_val`);
|
|
805
|
+
const someExpr = transformPureBody(someBranch, typeDecls);
|
|
806
|
+
if (!someExpr)
|
|
807
|
+
return null;
|
|
808
|
+
const noneExpr = transformPureBody(noneBranch, typeDecls);
|
|
809
|
+
if (!noneExpr)
|
|
810
|
+
return null;
|
|
811
|
+
const someReplaced = replaceVar(someExpr, optCheck.varName, { kind: "var", name: bound });
|
|
812
|
+
return {
|
|
813
|
+
kind: "match", scrutinee: optCheck.varName,
|
|
814
|
+
arms: [
|
|
815
|
+
{ pattern: `.some ${bound}`, body: someReplaced },
|
|
816
|
+
{ pattern: ".none", body: noneExpr },
|
|
817
|
+
],
|
|
818
|
+
};
|
|
819
|
+
}
|
|
529
820
|
const thenExpr = transformPureBody(s.then, typeDecls);
|
|
530
821
|
if (!thenExpr)
|
|
531
822
|
return null;
|
|
@@ -607,7 +898,7 @@ function transformTypeDecl(d) {
|
|
|
607
898
|
kind: "inductive", name: d.name,
|
|
608
899
|
constructors: d.variants.map(v => ({
|
|
609
900
|
name: v.name,
|
|
610
|
-
fields: v.fields.map(f => ({ name: f.name, type:
|
|
901
|
+
fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
|
|
611
902
|
})),
|
|
612
903
|
deriving: ["Repr", "Inhabited"],
|
|
613
904
|
};
|
|
@@ -615,38 +906,59 @@ function transformTypeDecl(d) {
|
|
|
615
906
|
else {
|
|
616
907
|
return {
|
|
617
908
|
kind: "structure", name: d.name,
|
|
618
|
-
fields: d.fields.map(f => ({ name: f.name, type:
|
|
909
|
+
fields: d.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
|
|
619
910
|
deriving: ["Repr", "Inhabited", "DecidableEq"],
|
|
620
911
|
};
|
|
621
912
|
}
|
|
622
913
|
}
|
|
623
914
|
// ── Helpers ──────────────────────────────────────────────────
|
|
915
|
+
/** Find parameter names that are reassigned anywhere in the body. */
|
|
916
|
+
function findReassignedNames(stmts, names) {
|
|
917
|
+
const found = new Set();
|
|
918
|
+
function scan(stmts) {
|
|
919
|
+
for (const s of stmts) {
|
|
920
|
+
if (s.kind === "assign" && names.has(s.target))
|
|
921
|
+
found.add(s.target);
|
|
922
|
+
if (s.kind === "ghostAssign" && names.has(s.target))
|
|
923
|
+
found.add(s.target);
|
|
924
|
+
// Mutating collection calls: s.add(x), m.set(k,v), s.delete(x), arr.push(x)
|
|
925
|
+
if (s.kind === "expr" && s.expr.kind === "call" && s.expr.fn.kind === "field" &&
|
|
926
|
+
s.expr.fn.obj.kind === "var" && names.has(s.expr.fn.obj.name) &&
|
|
927
|
+
["add", "set", "delete", "push"].includes(s.expr.fn.field)) {
|
|
928
|
+
found.add(s.expr.fn.obj.name);
|
|
929
|
+
}
|
|
930
|
+
if (s.kind === "if") {
|
|
931
|
+
scan(s.then);
|
|
932
|
+
scan(s.else);
|
|
933
|
+
}
|
|
934
|
+
if (s.kind === "while")
|
|
935
|
+
scan(s.body);
|
|
936
|
+
if (s.kind === "forof")
|
|
937
|
+
scan(s.body);
|
|
938
|
+
if (s.kind === "switch") {
|
|
939
|
+
for (const c of s.cases)
|
|
940
|
+
scan(c.body);
|
|
941
|
+
scan(s.defaultBody);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
scan(stmts);
|
|
946
|
+
return found;
|
|
947
|
+
}
|
|
624
948
|
/** Replace all occurrences of a variable name with a new expression. */
|
|
625
949
|
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
|
-
}
|
|
950
|
+
return mapExpr(e, x => {
|
|
951
|
+
if (x.kind === "var" && x.name === name)
|
|
952
|
+
return replacement;
|
|
953
|
+
// Don't descend past bindings that shadow the name
|
|
954
|
+
if (x.kind === "forall" && x.var === name)
|
|
955
|
+
return x;
|
|
956
|
+
if (x.kind === "exists" && x.var === name)
|
|
957
|
+
return x;
|
|
958
|
+
if (x.kind === "let" && x.name === name)
|
|
959
|
+
return { ...x, value: replaceVar(x.value, name, replacement) };
|
|
960
|
+
return null;
|
|
961
|
+
});
|
|
650
962
|
}
|
|
651
963
|
// ── Top-level transform ──────────────────────────────────────
|
|
652
964
|
/** Transform for Dafny backend — same logic, Dafny options. */
|
|
@@ -661,7 +973,15 @@ export function transformModuleDafny(mod) {
|
|
|
661
973
|
}
|
|
662
974
|
}
|
|
663
975
|
export function transformModule(mod, specImport) {
|
|
976
|
+
_forofCounters.clear();
|
|
664
977
|
const typeDecls = mod.typeDecls.map(transformTypeDecl);
|
|
978
|
+
// Module-level constants
|
|
979
|
+
const constDecls = (mod.constants ?? []).map(c => ({
|
|
980
|
+
kind: "const",
|
|
981
|
+
name: c.name,
|
|
982
|
+
type: c.ty,
|
|
983
|
+
value: transformExpr(c.value),
|
|
984
|
+
}));
|
|
665
985
|
// Pure function mirrors
|
|
666
986
|
const pureDefs = [];
|
|
667
987
|
for (const fn of mod.functions) {
|
|
@@ -676,8 +996,8 @@ export function transformModule(mod, specImport) {
|
|
|
676
996
|
pureDefs.push({
|
|
677
997
|
kind: "def",
|
|
678
998
|
name: fn.name,
|
|
679
|
-
params: fn.params.map(p => ({ name: p.name, type:
|
|
680
|
-
returnType:
|
|
999
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1000
|
+
returnType: fn.returnTy,
|
|
681
1001
|
requires: fn.requires.map(transformExpr),
|
|
682
1002
|
ensures,
|
|
683
1003
|
body,
|
|
@@ -686,9 +1006,6 @@ export function transformModule(mod, specImport) {
|
|
|
686
1006
|
const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
|
|
687
1007
|
// Types file
|
|
688
1008
|
const typesImports = ["LemmaScript"];
|
|
689
|
-
for (const m of usedImports)
|
|
690
|
-
typesImports.push(MODULE_IMPORTS[m] ?? m);
|
|
691
|
-
usedImports.clear();
|
|
692
1009
|
let typesFile = null;
|
|
693
1010
|
const pureNamespace = pureDefs.length > 0
|
|
694
1011
|
? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
|
|
@@ -713,25 +1030,54 @@ export function transformModule(mod, specImport) {
|
|
|
713
1030
|
else
|
|
714
1031
|
ensures.push(transformExpr(e));
|
|
715
1032
|
}
|
|
716
|
-
|
|
1033
|
+
_forofCounters.clear();
|
|
1034
|
+
let body = pureDefNames.has(fn.name)
|
|
717
1035
|
? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
|
|
718
1036
|
: transformStmts(fn.body, mod.typeDecls);
|
|
1037
|
+
// Shadow reassigned parameters with mutable locals
|
|
1038
|
+
const paramNames = new Set(fn.params.map(p => p.name));
|
|
1039
|
+
const reassigned = findReassignedNames(fn.body, paramNames);
|
|
1040
|
+
if (reassigned.size > 0) {
|
|
1041
|
+
const shadows = fn.params
|
|
1042
|
+
.filter(p => reassigned.has(p.name))
|
|
1043
|
+
.map(p => ({ kind: "let", name: p.name, type: p.ty, mutable: true, value: { kind: "var", name: p.name } }));
|
|
1044
|
+
body = [...shadows, ...body];
|
|
1045
|
+
}
|
|
719
1046
|
return {
|
|
720
1047
|
kind: "method",
|
|
721
1048
|
name: fn.name,
|
|
722
|
-
params: fn.params.map(p => ({ name: p.name, type:
|
|
723
|
-
returnType:
|
|
1049
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1050
|
+
returnType: fn.returnTy,
|
|
724
1051
|
requires: fn.requires.map(transformExpr),
|
|
725
1052
|
ensures,
|
|
726
1053
|
body,
|
|
727
1054
|
};
|
|
728
1055
|
});
|
|
1056
|
+
// Class declarations
|
|
1057
|
+
const classDecls = (mod.classes ?? []).map(cls => {
|
|
1058
|
+
const classMethods = cls.methods.map(fn => {
|
|
1059
|
+
const ensures = fn.ensures.map(transformExpr);
|
|
1060
|
+
_forofCounters.clear();
|
|
1061
|
+
const body = transformStmts(fn.body, mod.typeDecls);
|
|
1062
|
+
return {
|
|
1063
|
+
kind: "method",
|
|
1064
|
+
name: fn.name,
|
|
1065
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1066
|
+
returnType: fn.returnTy,
|
|
1067
|
+
requires: fn.requires.map(transformExpr),
|
|
1068
|
+
ensures,
|
|
1069
|
+
body,
|
|
1070
|
+
};
|
|
1071
|
+
});
|
|
1072
|
+
return {
|
|
1073
|
+
kind: "class",
|
|
1074
|
+
name: cls.name,
|
|
1075
|
+
fields: cls.fields.map(f => ({ name: f.name, type: f.ty })),
|
|
1076
|
+
methods: classMethods,
|
|
1077
|
+
};
|
|
1078
|
+
});
|
|
729
1079
|
const defImport = specImport ?? (typesFile ? `«${base}.types»` : null);
|
|
730
1080
|
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
1081
|
const defFile = {
|
|
736
1082
|
comment: " Generated by lsc from " + (mod.file.split("/").pop() ?? "") + "\n Do not edit — re-run `lsc gen` to regenerate.",
|
|
737
1083
|
imports: defBaseImports,
|
|
@@ -739,7 +1085,7 @@ export function transformModule(mod, specImport) {
|
|
|
739
1085
|
{ key: "loom.semantics.termination", value: '"total"' },
|
|
740
1086
|
{ key: "loom.semantics.choice", value: '"demonic"' },
|
|
741
1087
|
],
|
|
742
|
-
decls: methods,
|
|
1088
|
+
decls: [...constDecls, ...methods, ...classDecls],
|
|
743
1089
|
};
|
|
744
1090
|
return { typesFile, defFile };
|
|
745
1091
|
}
|