lemmascript 0.0.1 → 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/LICENSE +21 -0
- package/README.md +94 -3
- package/package.json +30 -20
- package/tools/dist/dafny-commands.js +98 -0
- package/tools/dist/dafny-emit.js +738 -0
- package/tools/dist/emit.js +253 -0
- package/tools/dist/extract.js +806 -0
- package/tools/dist/ir.js +7 -0
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +393 -0
- package/tools/dist/lsc.js +119 -0
- package/tools/dist/rawir.js +10 -0
- package/tools/dist/resolve.js +717 -0
- package/tools/dist/specparser.js +305 -0
- package/tools/dist/transform.js +1091 -0
- package/tools/dist/typedir.js +7 -0
- package/tools/dist/types.js +71 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -4
- package/src/index.ts +0 -1
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform — Typed IR → Backend IR.
|
|
3
|
+
*
|
|
4
|
+
* Consumes resolved types and classifications.
|
|
5
|
+
* No type lookups, no string parsing, no re-inference.
|
|
6
|
+
*/
|
|
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
|
+
}
|
|
69
|
+
export const LEAN_OPTIONS = {
|
|
70
|
+
backend: "lean",
|
|
71
|
+
monadic: true,
|
|
72
|
+
};
|
|
73
|
+
export const DAFNY_OPTIONS = {
|
|
74
|
+
backend: "dafny",
|
|
75
|
+
monadic: false,
|
|
76
|
+
};
|
|
77
|
+
/** Active options — set before each transform call. */
|
|
78
|
+
let _opts = LEAN_OPTIONS;
|
|
79
|
+
/** Prefix match-bound field names to avoid capturing user variables. */
|
|
80
|
+
function matchBinder(fieldName) {
|
|
81
|
+
return `_${fieldName}`;
|
|
82
|
+
}
|
|
83
|
+
const _forofCounters = new Map();
|
|
84
|
+
function isNat(ty) { return ty.kind === "nat"; }
|
|
85
|
+
function isArray(ty) { return ty.kind === "array"; }
|
|
86
|
+
function isUser(ty) { return ty.kind === "user"; }
|
|
87
|
+
/** Check if transformed lambda body contains monadic binds. */
|
|
88
|
+
function isMonadicBody(stmts) {
|
|
89
|
+
for (const s of stmts) {
|
|
90
|
+
if (s.kind === "let-bind" || s.kind === "bind")
|
|
91
|
+
return true;
|
|
92
|
+
if (s.kind === "if" && (isMonadicBody(s.then) || isMonadicBody(s.else)))
|
|
93
|
+
return true;
|
|
94
|
+
if (s.kind === "while" && isMonadicBody(s.body))
|
|
95
|
+
return true;
|
|
96
|
+
if (s.kind === "forin" && isMonadicBody(s.body))
|
|
97
|
+
return true;
|
|
98
|
+
if (s.kind === "match") {
|
|
99
|
+
for (const arm of s.arms)
|
|
100
|
+
if (isMonadicBody(arm.body))
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// ── Transform expressions ────────────────────────────────────
|
|
107
|
+
/** Prop-valued operators (for specs/invariants). */
|
|
108
|
+
const OP_MAP = {
|
|
109
|
+
"===": "=", "!==": "≠", ">=": "≥", "<=": "≤", ">": ">", "<": "<",
|
|
110
|
+
"&&": "∧", "||": "∨", "+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
|
|
111
|
+
"==": "=", "!=": "≠",
|
|
112
|
+
};
|
|
113
|
+
/** Bool-valued operators (for code-level conditions needing Decidable). */
|
|
114
|
+
const BOOL_OP_MAP = {
|
|
115
|
+
...OP_MAP, "===": "==", "!==": "!=",
|
|
116
|
+
};
|
|
117
|
+
function transformExpr(e) { return lowerExpr(e, null); }
|
|
118
|
+
/**
|
|
119
|
+
* Lower a typed expression to Backend IR.
|
|
120
|
+
*
|
|
121
|
+
* When `binds` is non-null, embedded method calls are extracted into
|
|
122
|
+
* `let ← ` binds (monadic lifting / selective ANF). Lifting propagates
|
|
123
|
+
* through binop, unop, and call arguments — the expression kinds where
|
|
124
|
+
* a method call can appear inline in TS. It does NOT propagate into
|
|
125
|
+
* field, index, record, forall, or exists sub-expressions.
|
|
126
|
+
*/
|
|
127
|
+
function lowerExpr(e, binds) {
|
|
128
|
+
// Monadic lifting: extract embedded method calls to let-binds
|
|
129
|
+
// Pass binds through to args so nested method calls are also lifted
|
|
130
|
+
if (binds && e.kind === "call" && e.callKind === "method") {
|
|
131
|
+
const name = `_t${_liftCounter++}`;
|
|
132
|
+
const fn = e.fn.kind === "var" ? e.fn.name : `${lowerExpr(e.fn, binds)}`;
|
|
133
|
+
const args = e.args.map(a => lowerExpr(a, binds));
|
|
134
|
+
binds.push({ kind: "let-bind", name, value: { kind: "app", fn, args } });
|
|
135
|
+
return { kind: "var", name };
|
|
136
|
+
}
|
|
137
|
+
switch (e.kind) {
|
|
138
|
+
case "var": return { kind: "var", name: e.name };
|
|
139
|
+
case "num": return { kind: "num", value: e.value };
|
|
140
|
+
case "bool": return { kind: "bool", value: e.value };
|
|
141
|
+
case "result": return { kind: "var", name: "res" };
|
|
142
|
+
case "str":
|
|
143
|
+
if (e.ty.kind === "user")
|
|
144
|
+
return { kind: "constructor", name: e.value, type: e.ty.name };
|
|
145
|
+
return { kind: "str", value: e.value };
|
|
146
|
+
case "unop":
|
|
147
|
+
if (e.op === "-" && e.expr.kind === "num")
|
|
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
|
+
}
|
|
163
|
+
return { kind: "unop", op: e.op === "!" ? "¬" : e.op, expr: lowerExpr(e.expr, binds) };
|
|
164
|
+
case "binop": {
|
|
165
|
+
// Implication: flatten (A && B) ==> C → implies [A, B] C
|
|
166
|
+
// Spec-only — no lifting through premises/conclusion.
|
|
167
|
+
if (e.op === "==>") {
|
|
168
|
+
const { premises, conclusion } = flattenImpl(e);
|
|
169
|
+
return { kind: "implies", premises: premises.map(transformExpr), conclusion: transformExpr(conclusion) };
|
|
170
|
+
}
|
|
171
|
+
// Discriminant check: x.discriminant === "foo" → x = .foo (before generic string literal comparison)
|
|
172
|
+
if ((e.op === "===" || e.op === "!==") && e.left.kind === "field" && e.left.isDiscriminant && e.right.kind === "str") {
|
|
173
|
+
const objTy = e.left.obj.ty.kind === "user" ? e.left.obj.ty.name : undefined;
|
|
174
|
+
return {
|
|
175
|
+
kind: "binop",
|
|
176
|
+
op: e.op === "===" ? "=" : "≠",
|
|
177
|
+
left: transformExpr(e.left.obj),
|
|
178
|
+
right: { kind: "constructor", name: e.right.value, type: objTy },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// String literal comparison — constructor if user type, string literal if string
|
|
182
|
+
if ((e.op === "===" || e.op === "!==") && e.right.kind === "str") {
|
|
183
|
+
const left = lowerExpr(e.left, binds);
|
|
184
|
+
const leftTy = e.left.ty.kind === "user" ? e.left.ty.name : undefined;
|
|
185
|
+
const right = isUser(e.left.ty)
|
|
186
|
+
? { kind: "constructor", name: e.right.value, type: leftTy }
|
|
187
|
+
: { kind: "str", value: e.right.value };
|
|
188
|
+
return { kind: "binop", op: e.op === "===" ? "=" : "≠", left, right };
|
|
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
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
kind: "binop",
|
|
233
|
+
op: OP_MAP[e.op] ?? e.op,
|
|
234
|
+
left: lowerExpr(e.left, binds),
|
|
235
|
+
right: lowerExpr(e.right, binds),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
case "field":
|
|
239
|
+
if (e.field === "length" && isArray(e.obj.ty))
|
|
240
|
+
return { kind: "field", obj: transformExpr(e.obj), field: "size" };
|
|
241
|
+
if (e.field === "length" && e.obj.ty.kind === "string")
|
|
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" };
|
|
245
|
+
return { kind: "field", obj: transformExpr(e.obj), field: e.field };
|
|
246
|
+
case "index": {
|
|
247
|
+
const idx = transformExpr(e.idx);
|
|
248
|
+
const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
|
|
249
|
+
return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
|
|
250
|
+
}
|
|
251
|
+
case "call": {
|
|
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
|
|
260
|
+
if (e.fn.kind === "field" && e.fn.field === "floor" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
|
|
261
|
+
const arg = e.args[0];
|
|
262
|
+
if (arg.ty.kind === "real")
|
|
263
|
+
return { kind: "app", fn: "FloorReal", args: [lowerExpr(arg, binds)] };
|
|
264
|
+
if (_opts.backend === "dafny" && arg.kind === "binop" && arg.op === "/")
|
|
265
|
+
return { kind: "app", fn: "JSFloorDiv", args: [lowerExpr(arg.left, binds), lowerExpr(arg.right, binds)] };
|
|
266
|
+
return lowerExpr(arg, binds);
|
|
267
|
+
}
|
|
268
|
+
// Method call: receiver.method(args) → methodCall node
|
|
269
|
+
if (e.fn.kind === "field") {
|
|
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 };
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
if (e.fn.kind !== "var")
|
|
295
|
+
throw new Error(`Unsupported call expression: ${e.fn.kind}`);
|
|
296
|
+
const prefix = e.callKind === "spec-pure" && _opts.backend === "lean" ? "Pure." : "";
|
|
297
|
+
return { kind: "app", fn: prefix + e.fn.name, args: e.args.map(a => lowerExpr(a, binds)) };
|
|
298
|
+
}
|
|
299
|
+
case "record":
|
|
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) })) };
|
|
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" };
|
|
306
|
+
return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
|
|
307
|
+
case "lambda":
|
|
308
|
+
return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: p.ty })), body: transformStmts(e.body, []) };
|
|
309
|
+
case "forall":
|
|
310
|
+
return { kind: "forall", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
311
|
+
case "exists":
|
|
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 };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function flattenImpl(e) {
|
|
345
|
+
if (e.kind === "binop" && e.op === "==>") {
|
|
346
|
+
const lhs = splitConj(e.left);
|
|
347
|
+
const rest = flattenImpl(e.right);
|
|
348
|
+
return { premises: [...lhs, ...rest.premises], conclusion: rest.conclusion };
|
|
349
|
+
}
|
|
350
|
+
return { premises: [], conclusion: e };
|
|
351
|
+
}
|
|
352
|
+
function splitConj(e) {
|
|
353
|
+
if (e.kind === "binop" && e.op === "&&")
|
|
354
|
+
return [...splitConj(e.left), ...splitConj(e.right)];
|
|
355
|
+
return [e];
|
|
356
|
+
}
|
|
357
|
+
// ── Ensures-to-match for discriminated unions ────────────────
|
|
358
|
+
function ensuresToMatch(e, typeDecls) {
|
|
359
|
+
if (e.kind !== "binop" || e.op !== "==>")
|
|
360
|
+
return null;
|
|
361
|
+
if (e.left.kind !== "binop" || e.left.op !== "===")
|
|
362
|
+
return null;
|
|
363
|
+
if (e.left.left.kind !== "field" || !e.left.left.isDiscriminant || e.left.right.kind !== "str")
|
|
364
|
+
return null;
|
|
365
|
+
const obj = e.left.left.obj;
|
|
366
|
+
if (obj.kind !== "var" || obj.ty.kind !== "user")
|
|
367
|
+
return null;
|
|
368
|
+
const typeName = obj.ty.name;
|
|
369
|
+
const decl = typeDecls.find(d => d.name === typeName && d.kind === "discriminated-union");
|
|
370
|
+
if (!decl)
|
|
371
|
+
return null;
|
|
372
|
+
const variantName = e.left.right.value;
|
|
373
|
+
const variant = decl.variants?.find(v => v.name === variantName);
|
|
374
|
+
if (!variant)
|
|
375
|
+
return null;
|
|
376
|
+
const fields = variant.fields;
|
|
377
|
+
const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${variantName}`;
|
|
378
|
+
let rhs = transformExpr(e.right);
|
|
379
|
+
rhs = replaceFieldAccess(rhs, obj.name, fields);
|
|
380
|
+
return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
|
|
381
|
+
}
|
|
382
|
+
function replaceFieldAccess(e, varName, fields) {
|
|
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
|
+
});
|
|
394
|
+
}
|
|
395
|
+
// ── Transform statements ─────────────────────────────────────
|
|
396
|
+
function transformStmts(stmts, typeDecls) {
|
|
397
|
+
const result = [];
|
|
398
|
+
let i = 0;
|
|
399
|
+
while (i < stmts.length) {
|
|
400
|
+
const s = stmts[i];
|
|
401
|
+
// Detect discriminant if-chain → match
|
|
402
|
+
if (s.kind === "if") {
|
|
403
|
+
const chain = detectDiscriminantChain(stmts.slice(i));
|
|
404
|
+
if (chain) {
|
|
405
|
+
result.push(emitMatchStmt(chain.chain, typeDecls));
|
|
406
|
+
i += chain.consumed;
|
|
407
|
+
continue;
|
|
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
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Transform for-of → for-in over range
|
|
424
|
+
if (s.kind === "forof") {
|
|
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}`;
|
|
468
|
+
const idx = { kind: "var", name: idxName };
|
|
469
|
+
const arrSize = { kind: "field", obj: iterExpr, field: "size" };
|
|
470
|
+
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
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 };
|
|
474
|
+
result.push({
|
|
475
|
+
kind: "forin",
|
|
476
|
+
idx: idxName,
|
|
477
|
+
bound: arrSize,
|
|
478
|
+
invariants: [boundInv, ...s.invariants.map(transformExpr)],
|
|
479
|
+
body: [letElem, ...bodyStmts],
|
|
480
|
+
});
|
|
481
|
+
i++;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
result.push(...transformStmt(s, typeDecls));
|
|
485
|
+
i++;
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
let _liftCounter = 0;
|
|
490
|
+
function liftMethodCalls(e) {
|
|
491
|
+
const binds = [];
|
|
492
|
+
return { binds, expr: lowerExpr(e, binds) };
|
|
493
|
+
}
|
|
494
|
+
// ── Transform statements ─────────────────────────────────────
|
|
495
|
+
function transformStmt(s, typeDecls) {
|
|
496
|
+
switch (s.kind) {
|
|
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
|
+
}
|
|
561
|
+
const { binds, expr } = liftMethodCalls(s.init);
|
|
562
|
+
return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
|
|
563
|
+
}
|
|
564
|
+
case "assign": {
|
|
565
|
+
// Top-level method call → direct monadic bind, no lifting needed
|
|
566
|
+
if (s.value.kind === "call" && s.value.callKind === "method")
|
|
567
|
+
return [{ kind: "bind", target: s.target, value: transformExpr(s.value) }];
|
|
568
|
+
const { binds, expr } = liftMethodCalls(s.value);
|
|
569
|
+
return [...binds, { kind: "assign", target: s.target, value: expr }];
|
|
570
|
+
}
|
|
571
|
+
case "return": {
|
|
572
|
+
const { binds, expr } = liftMethodCalls(s.value);
|
|
573
|
+
return [...binds, { kind: "return", value: expr }];
|
|
574
|
+
}
|
|
575
|
+
case "break": return [{ kind: "break" }];
|
|
576
|
+
case "continue": return [{ kind: "continue" }];
|
|
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
|
+
}
|
|
609
|
+
const { binds, expr } = liftMethodCalls(s.expr);
|
|
610
|
+
return [...binds, { kind: "assign", target: "_", value: expr }];
|
|
611
|
+
}
|
|
612
|
+
case "if": {
|
|
613
|
+
// Lift from condition only (Lean rule: don't lift from branches)
|
|
614
|
+
const { binds, expr: cond } = liftMethodCalls(s.cond);
|
|
615
|
+
return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
616
|
+
}
|
|
617
|
+
case "while":
|
|
618
|
+
return [{
|
|
619
|
+
kind: "while",
|
|
620
|
+
cond: transformExpr(s.cond),
|
|
621
|
+
invariants: s.invariants.map(transformExpr),
|
|
622
|
+
decreasing: s.decreases ? transformExpr(s.decreases) : null,
|
|
623
|
+
doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
|
|
624
|
+
body: transformStmts(s.body, typeDecls),
|
|
625
|
+
}];
|
|
626
|
+
case "throw":
|
|
627
|
+
return [{ kind: "assert", expr: { kind: "bool", value: false } }];
|
|
628
|
+
case "forof":
|
|
629
|
+
throw new Error("forof should be transformed to forin (range loop) in transformStmts");
|
|
630
|
+
case "switch":
|
|
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) }];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function detectDiscriminantChain(stmts) {
|
|
641
|
+
if (stmts.length === 0 || stmts[0].kind !== "if")
|
|
642
|
+
return null;
|
|
643
|
+
const first = parseDiscriminantCond(stmts[0].cond);
|
|
644
|
+
if (!first)
|
|
645
|
+
return null;
|
|
646
|
+
const cases = [];
|
|
647
|
+
// Follow else branches within one if-else-if tree
|
|
648
|
+
function collectElse(s) {
|
|
649
|
+
const p = parseDiscriminantCond(s.cond);
|
|
650
|
+
if (!p || p.varName !== first.varName)
|
|
651
|
+
return [s];
|
|
652
|
+
cases.push({ variant: p.variant, body: s.then });
|
|
653
|
+
if (s.else.length === 0)
|
|
654
|
+
return [];
|
|
655
|
+
if (s.else.length === 1 && s.else[0].kind === "if")
|
|
656
|
+
return collectElse(s.else[0]);
|
|
657
|
+
return s.else;
|
|
658
|
+
}
|
|
659
|
+
// Walk consecutive top-level ifs on the same discriminant
|
|
660
|
+
let consumed = 0;
|
|
661
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
662
|
+
const s = stmts[i];
|
|
663
|
+
if (s.kind !== "if")
|
|
664
|
+
break;
|
|
665
|
+
const p = parseDiscriminantCond(s.cond);
|
|
666
|
+
if (!p || p.varName !== first.varName)
|
|
667
|
+
break;
|
|
668
|
+
cases.push({ variant: p.variant, body: s.then });
|
|
669
|
+
consumed = i + 1;
|
|
670
|
+
if (s.else.length > 0) {
|
|
671
|
+
const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
|
|
672
|
+
return cases.length > 0 ? { chain: { ...first, cases, fallthrough: ft }, consumed } : null;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (cases.length === 0)
|
|
676
|
+
return null;
|
|
677
|
+
return { chain: { ...first, cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
|
|
678
|
+
}
|
|
679
|
+
function parseDiscriminantCond(cond) {
|
|
680
|
+
// Pattern: x.discriminant === "variant"
|
|
681
|
+
if (cond.kind !== "binop" || cond.op !== "===" || cond.right.kind !== "str")
|
|
682
|
+
return null;
|
|
683
|
+
if (cond.left.kind !== "field" || !cond.left.isDiscriminant)
|
|
684
|
+
return null;
|
|
685
|
+
if (cond.left.obj.kind !== "var" || cond.left.obj.ty.kind !== "user")
|
|
686
|
+
return null;
|
|
687
|
+
return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
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
|
+
}
|
|
724
|
+
function emitMatchStmt(chain, typeDecls) {
|
|
725
|
+
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
726
|
+
const arms = chain.cases.map(c => {
|
|
727
|
+
const variant = decl?.variants?.find(v => v.name === c.variant);
|
|
728
|
+
const fields = variant?.fields ?? [];
|
|
729
|
+
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
|
|
730
|
+
let body = transformStmts(c.body, typeDecls);
|
|
731
|
+
body = replaceFieldAccessInStmts(body, chain.varName, fields);
|
|
732
|
+
return { pattern, body };
|
|
733
|
+
});
|
|
734
|
+
if (chain.fallthrough.length > 0)
|
|
735
|
+
arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
|
|
736
|
+
return { kind: "match", scrutinee: chain.varName, arms };
|
|
737
|
+
}
|
|
738
|
+
function emitSwitchStmt(s, typeDecls) {
|
|
739
|
+
const varName = s.expr.kind === "var" ? s.expr.name : "?";
|
|
740
|
+
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
|
|
741
|
+
const decl = typeName ? typeDecls.find(d => d.name === typeName) : undefined;
|
|
742
|
+
const arms = s.cases.map(c => {
|
|
743
|
+
const variant = decl?.variants?.find(v => v.name === c.label);
|
|
744
|
+
const fields = variant?.fields ?? [];
|
|
745
|
+
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
|
|
746
|
+
let body = transformStmts(c.body, typeDecls);
|
|
747
|
+
body = replaceFieldAccessInStmts(body, varName, fields);
|
|
748
|
+
return { pattern, body };
|
|
749
|
+
});
|
|
750
|
+
if (s.defaultBody.length > 0)
|
|
751
|
+
arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
|
|
752
|
+
return { kind: "match", scrutinee: varName, arms };
|
|
753
|
+
}
|
|
754
|
+
function replaceFieldAccessInStmts(stmts, varName, fields) {
|
|
755
|
+
if (fields.length === 0)
|
|
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
|
+
};
|
|
765
|
+
const result = [];
|
|
766
|
+
for (const s of stmts) {
|
|
767
|
+
// If a let shadows the matched variable, stop replacing from here on
|
|
768
|
+
if (s.kind === "let" && s.name === varName) {
|
|
769
|
+
result.push(s.value ? { ...s, value: mapExpr(s.value, f) } : s);
|
|
770
|
+
result.push(...stmts.slice(result.length));
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
result.push(mapStmt(s, f));
|
|
774
|
+
}
|
|
775
|
+
return result;
|
|
776
|
+
}
|
|
777
|
+
// ── Pure function generation ─────────────────────────────────
|
|
778
|
+
function transformPureBody(stmts, typeDecls) {
|
|
779
|
+
// Detect discriminant if-chain
|
|
780
|
+
if (stmts.length > 0 && stmts[0].kind === "if") {
|
|
781
|
+
const chain = detectDiscriminantChain(stmts);
|
|
782
|
+
if (chain)
|
|
783
|
+
return transformPureMatch(chain.chain, typeDecls);
|
|
784
|
+
}
|
|
785
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
786
|
+
const s = stmts[i];
|
|
787
|
+
const rest = stmts.slice(i + 1);
|
|
788
|
+
switch (s.kind) {
|
|
789
|
+
case "return": return transformExpr(s.value);
|
|
790
|
+
case "let": {
|
|
791
|
+
const restExpr = transformPureBody(rest, typeDecls);
|
|
792
|
+
if (!restExpr)
|
|
793
|
+
return null;
|
|
794
|
+
return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
|
|
795
|
+
}
|
|
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
|
+
}
|
|
820
|
+
const thenExpr = transformPureBody(s.then, typeDecls);
|
|
821
|
+
if (!thenExpr)
|
|
822
|
+
return null;
|
|
823
|
+
const elseBranch = s.else.length > 0 ? s.else : rest;
|
|
824
|
+
const elseExpr = transformPureBody(elseBranch, typeDecls);
|
|
825
|
+
if (!elseExpr)
|
|
826
|
+
return null;
|
|
827
|
+
return { kind: "if", cond: transformExpr(s.cond), then: thenExpr, else: elseExpr };
|
|
828
|
+
}
|
|
829
|
+
case "switch": return transformPureSwitch(s, typeDecls);
|
|
830
|
+
default: return null;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
function transformPureSwitch(s, typeDecls) {
|
|
836
|
+
const decl = typeDecls.find(d => d.name === (s.expr.ty.kind === "user" ? s.expr.ty.name : ""));
|
|
837
|
+
if (!decl)
|
|
838
|
+
return null;
|
|
839
|
+
const arms = [];
|
|
840
|
+
for (const c of s.cases) {
|
|
841
|
+
const variant = decl.variants?.find(v => v.name === c.label);
|
|
842
|
+
const fields = variant?.fields ?? [];
|
|
843
|
+
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
|
|
844
|
+
let body = transformPureBody(c.body, typeDecls);
|
|
845
|
+
if (!body)
|
|
846
|
+
return null;
|
|
847
|
+
if (fields.length > 0 && s.expr.kind === "var")
|
|
848
|
+
body = replaceFieldAccess(body, s.expr.name, fields);
|
|
849
|
+
arms.push({ pattern, body });
|
|
850
|
+
}
|
|
851
|
+
if (s.defaultBody.length > 0) {
|
|
852
|
+
const body = transformPureBody(s.defaultBody, typeDecls);
|
|
853
|
+
if (!body)
|
|
854
|
+
return null;
|
|
855
|
+
arms.push({ pattern: "_", body });
|
|
856
|
+
}
|
|
857
|
+
if (s.expr.kind !== "var")
|
|
858
|
+
return null;
|
|
859
|
+
return { kind: "match", scrutinee: s.expr.name, arms };
|
|
860
|
+
}
|
|
861
|
+
function transformPureMatch(chain, typeDecls) {
|
|
862
|
+
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
863
|
+
const arms = [];
|
|
864
|
+
for (const c of chain.cases) {
|
|
865
|
+
const variant = decl?.variants?.find(v => v.name === c.variant);
|
|
866
|
+
const fields = variant?.fields ?? [];
|
|
867
|
+
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
|
|
868
|
+
let body = transformPureBody(c.body, typeDecls);
|
|
869
|
+
if (!body)
|
|
870
|
+
return null;
|
|
871
|
+
if (fields.length > 0)
|
|
872
|
+
body = replaceFieldAccess(body, chain.varName, fields);
|
|
873
|
+
arms.push({ pattern, body });
|
|
874
|
+
}
|
|
875
|
+
// Idiomatic TS often has an unreachable fallthrough after exhaustive if-chains on
|
|
876
|
+
// discriminated unions. Skip the catch-all arm when all variants are matched,
|
|
877
|
+
// since Lean errors on redundant match arms.
|
|
878
|
+
const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
|
|
879
|
+
if (chain.fallthrough.length > 0 && !allCovered) {
|
|
880
|
+
const body = transformPureBody(chain.fallthrough, typeDecls);
|
|
881
|
+
if (!body)
|
|
882
|
+
return null;
|
|
883
|
+
arms.push({ pattern: "_", body });
|
|
884
|
+
}
|
|
885
|
+
return { kind: "match", scrutinee: chain.varName, arms };
|
|
886
|
+
}
|
|
887
|
+
// ── Generate type declarations ───────────────────────────────
|
|
888
|
+
function transformTypeDecl(d) {
|
|
889
|
+
if (d.kind === "string-union") {
|
|
890
|
+
return {
|
|
891
|
+
kind: "inductive", name: d.name,
|
|
892
|
+
constructors: d.values.map(v => ({ name: v, fields: [] })),
|
|
893
|
+
deriving: ["Repr", "Inhabited", "DecidableEq"],
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
else if (d.kind === "discriminated-union") {
|
|
897
|
+
return {
|
|
898
|
+
kind: "inductive", name: d.name,
|
|
899
|
+
constructors: d.variants.map(v => ({
|
|
900
|
+
name: v.name,
|
|
901
|
+
fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
|
|
902
|
+
})),
|
|
903
|
+
deriving: ["Repr", "Inhabited"],
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
return {
|
|
908
|
+
kind: "structure", name: d.name,
|
|
909
|
+
fields: d.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
|
|
910
|
+
deriving: ["Repr", "Inhabited", "DecidableEq"],
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
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
|
+
}
|
|
948
|
+
/** Replace all occurrences of a variable name with a new expression. */
|
|
949
|
+
function replaceVar(e, name, replacement) {
|
|
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
|
+
});
|
|
962
|
+
}
|
|
963
|
+
// ── Top-level transform ──────────────────────────────────────
|
|
964
|
+
/** Transform for Dafny backend — same logic, Dafny options. */
|
|
965
|
+
export function transformModuleDafny(mod) {
|
|
966
|
+
const prev = _opts;
|
|
967
|
+
_opts = DAFNY_OPTIONS;
|
|
968
|
+
try {
|
|
969
|
+
return transformModule(mod);
|
|
970
|
+
}
|
|
971
|
+
finally {
|
|
972
|
+
_opts = prev;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
export function transformModule(mod, specImport) {
|
|
976
|
+
_forofCounters.clear();
|
|
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
|
+
}));
|
|
985
|
+
// Pure function mirrors
|
|
986
|
+
const pureDefs = [];
|
|
987
|
+
for (const fn of mod.functions) {
|
|
988
|
+
if (!fn.isPure)
|
|
989
|
+
continue;
|
|
990
|
+
const body = transformPureBody(fn.body, mod.typeDecls);
|
|
991
|
+
if (!body)
|
|
992
|
+
continue;
|
|
993
|
+
// For ensures, replace \result (→ "res") with the function call
|
|
994
|
+
const fnCall = { kind: "app", fn: fn.name, args: fn.params.map(p => ({ kind: "var", name: p.name })) };
|
|
995
|
+
const ensures = fn.ensures.map(e => replaceVar(transformExpr(e), "res", fnCall));
|
|
996
|
+
pureDefs.push({
|
|
997
|
+
kind: "def",
|
|
998
|
+
name: fn.name,
|
|
999
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1000
|
+
returnType: fn.returnTy,
|
|
1001
|
+
requires: fn.requires.map(transformExpr),
|
|
1002
|
+
ensures,
|
|
1003
|
+
body,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
|
|
1007
|
+
// Types file
|
|
1008
|
+
const typesImports = ["LemmaScript"];
|
|
1009
|
+
let typesFile = null;
|
|
1010
|
+
const pureNamespace = pureDefs.length > 0
|
|
1011
|
+
? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
|
|
1012
|
+
: [];
|
|
1013
|
+
if (typeDecls.length > 0 || pureDefs.length > 0) {
|
|
1014
|
+
typesFile = {
|
|
1015
|
+
comment: " Generated by lsc — Lean types and pure function mirrors.",
|
|
1016
|
+
imports: typesImports,
|
|
1017
|
+
options: [],
|
|
1018
|
+
decls: [...typeDecls, ...pureNamespace],
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
// Def file: Velvet methods
|
|
1022
|
+
// Pure functions get a thin wrapper that calls Pure.fnName
|
|
1023
|
+
const pureDefNames = new Set(pureDefs.map(d => d.name));
|
|
1024
|
+
const methods = mod.functions.map(fn => {
|
|
1025
|
+
const ensures = [];
|
|
1026
|
+
for (const e of fn.ensures) {
|
|
1027
|
+
const m = ensuresToMatch(e, mod.typeDecls);
|
|
1028
|
+
if (m)
|
|
1029
|
+
ensures.push(m);
|
|
1030
|
+
else
|
|
1031
|
+
ensures.push(transformExpr(e));
|
|
1032
|
+
}
|
|
1033
|
+
_forofCounters.clear();
|
|
1034
|
+
let body = pureDefNames.has(fn.name)
|
|
1035
|
+
? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
|
|
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
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
kind: "method",
|
|
1048
|
+
name: fn.name,
|
|
1049
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1050
|
+
returnType: fn.returnTy,
|
|
1051
|
+
requires: fn.requires.map(transformExpr),
|
|
1052
|
+
ensures,
|
|
1053
|
+
body,
|
|
1054
|
+
};
|
|
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
|
+
});
|
|
1079
|
+
const defImport = specImport ?? (typesFile ? `«${base}.types»` : null);
|
|
1080
|
+
const defBaseImports = defImport ? [defImport] : ["LemmaScript"];
|
|
1081
|
+
const defFile = {
|
|
1082
|
+
comment: " Generated by lsc from " + (mod.file.split("/").pop() ?? "") + "\n Do not edit — re-run `lsc gen` to regenerate.",
|
|
1083
|
+
imports: defBaseImports,
|
|
1084
|
+
options: [
|
|
1085
|
+
{ key: "loom.semantics.termination", value: '"total"' },
|
|
1086
|
+
{ key: "loom.semantics.choice", value: '"demonic"' },
|
|
1087
|
+
],
|
|
1088
|
+
decls: [...constDecls, ...methods, ...classDecls],
|
|
1089
|
+
};
|
|
1090
|
+
return { typesFile, defFile };
|
|
1091
|
+
}
|