lemmascript 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -31
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +47 -53
- package/tools/dist/dafny-emit.js +495 -93
- package/tools/dist/extract.js +847 -46
- package/tools/dist/ir.js +2 -2
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +397 -0
- package/tools/dist/lsc.js +62 -44
- package/tools/dist/resolve.js +500 -34
- package/tools/dist/specparser.js +66 -9
- package/tools/dist/transform.js +813 -202
- package/tools/dist/types.js +59 -13
package/tools/dist/resolve.js
CHANGED
|
@@ -6,6 +6,72 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { parseTsType } from "./types.js";
|
|
8
8
|
import { parseExpr } from "./specparser.js";
|
|
9
|
+
// ── Raw expression substitution ─────────────────────────────
|
|
10
|
+
let _synVarCounter = 0;
|
|
11
|
+
/**
|
|
12
|
+
* Structural equality for raw field-access chains (var and field nodes only).
|
|
13
|
+
* Exact within a single expression scope — raw IR has no bindings that could
|
|
14
|
+
* cause name collisions (those are introduced by resolve, which runs after).
|
|
15
|
+
*/
|
|
16
|
+
function rawExprEquals(a, b) {
|
|
17
|
+
if (a.kind === "var" && b.kind === "var")
|
|
18
|
+
return a.name === b.name;
|
|
19
|
+
if (a.kind === "field" && b.kind === "field")
|
|
20
|
+
return a.field === b.field && rawExprEquals(a.obj, b.obj);
|
|
21
|
+
if (a.kind === "call" && b.kind === "call")
|
|
22
|
+
return rawExprEquals(a.fn, b.fn) && a.args.length === b.args.length && a.args.every((arg, i) => rawExprEquals(arg, b.args[i]));
|
|
23
|
+
if (a.kind === "index" && b.kind === "index")
|
|
24
|
+
return rawExprEquals(a.obj, b.obj) && rawExprEquals(a.idx, b.idx);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/** Return the root variable name of a field-access chain, or null. */
|
|
28
|
+
function rawChainRoot(e) {
|
|
29
|
+
if (e.kind === "var")
|
|
30
|
+
return e.name;
|
|
31
|
+
if (e.kind === "field")
|
|
32
|
+
return rawChainRoot(e.obj);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Replace all occurrences of `target` in `expr` with `replacement`.
|
|
37
|
+
* Only matches field-access chains (see rawExprEquals). Stops at lambda
|
|
38
|
+
* boundaries that shadow the chain's root variable.
|
|
39
|
+
*/
|
|
40
|
+
function substituteRawExpr(expr, target, replacement) {
|
|
41
|
+
if (rawExprEquals(expr, target))
|
|
42
|
+
return replacement;
|
|
43
|
+
const root = rawChainRoot(target);
|
|
44
|
+
const sub = (e) => substituteRawExpr(e, target, replacement);
|
|
45
|
+
switch (expr.kind) {
|
|
46
|
+
case "var":
|
|
47
|
+
case "num":
|
|
48
|
+
case "str":
|
|
49
|
+
case "bool":
|
|
50
|
+
case "result":
|
|
51
|
+
case "havoc":
|
|
52
|
+
case "emptyCollection":
|
|
53
|
+
return expr;
|
|
54
|
+
case "binop": return { ...expr, left: sub(expr.left), right: sub(expr.right) };
|
|
55
|
+
case "unop": return { ...expr, expr: sub(expr.expr) };
|
|
56
|
+
case "call": return { ...expr, fn: sub(expr.fn), args: expr.args.map(sub) };
|
|
57
|
+
case "field": return { ...expr, obj: sub(expr.obj) };
|
|
58
|
+
case "index": return { ...expr, obj: sub(expr.obj), idx: sub(expr.idx) };
|
|
59
|
+
case "record":
|
|
60
|
+
return { ...expr, spread: expr.spread ? sub(expr.spread) : null,
|
|
61
|
+
fields: expr.fields.map(f => ({ ...f, value: sub(f.value) })) };
|
|
62
|
+
case "arrayLiteral": return { ...expr, elems: expr.elems.map(sub) };
|
|
63
|
+
case "conditional": return { ...expr, cond: sub(expr.cond), then: sub(expr.then), else: sub(expr.else) };
|
|
64
|
+
case "nonNull": return { ...expr, expr: sub(expr.expr) };
|
|
65
|
+
case "forall":
|
|
66
|
+
case "exists":
|
|
67
|
+
return { ...expr, body: sub(expr.body) };
|
|
68
|
+
case "lambda":
|
|
69
|
+
// Don't cross lambda boundaries that shadow the chain's root variable
|
|
70
|
+
if (root && expr.params.some(p => p.name === root))
|
|
71
|
+
return expr;
|
|
72
|
+
return { ...expr, body: Array.isArray(expr.body) ? expr.body : sub(expr.body) };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
9
75
|
function lookup(env, name) {
|
|
10
76
|
if (!env)
|
|
11
77
|
return undefined;
|
|
@@ -33,12 +99,96 @@ function coerceStr(expr, targetTy) {
|
|
|
33
99
|
return expr;
|
|
34
100
|
}
|
|
35
101
|
// ── Helpers ──────────────────────────────────────────────────
|
|
102
|
+
/** Detect `v !== undefined` or `undefined !== v` where v: optional<T>. */
|
|
103
|
+
function narrowOptional(cond, env) {
|
|
104
|
+
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
|
|
105
|
+
return null;
|
|
106
|
+
// v !== undefined OR undefined !== v
|
|
107
|
+
let varName = null;
|
|
108
|
+
if (cond.left.kind === "var" && cond.right.kind === "var" && cond.right.name === "undefined")
|
|
109
|
+
varName = cond.left.name;
|
|
110
|
+
if (cond.right.kind === "var" && cond.left.kind === "var" && cond.left.name === "undefined")
|
|
111
|
+
varName = cond.right.name;
|
|
112
|
+
if (!varName)
|
|
113
|
+
return null;
|
|
114
|
+
const ty = lookup(env, varName);
|
|
115
|
+
if (!ty || ty.kind !== "optional")
|
|
116
|
+
return null;
|
|
117
|
+
return { varName, innerTy: ty.inner, inThen: cond.op === "!==" };
|
|
118
|
+
}
|
|
119
|
+
/** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
|
|
120
|
+
function isRefMutableInTS(ty) {
|
|
121
|
+
return ty.kind === "array" || ty.kind === "map" || ty.kind === "set";
|
|
122
|
+
}
|
|
36
123
|
function findDecl(ctx, name) {
|
|
37
124
|
return ctx.typeDecls.find(d => d.name === name);
|
|
38
125
|
}
|
|
39
126
|
function getDiscriminant(ctx, typeName) {
|
|
40
127
|
return findDecl(ctx, typeName)?.discriminant;
|
|
41
128
|
}
|
|
129
|
+
/** Infer quantifier variable type from usage in body.
|
|
130
|
+
* If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
|
|
131
|
+
* return the collection's key type. Otherwise return null (default to int). */
|
|
132
|
+
function inferQuantVarType(varName, body, ctx) {
|
|
133
|
+
// Look for calls like map.has(k), map.get(k), or array.includes(k) where k is our variable
|
|
134
|
+
if (body.kind === "call" && body.fn.kind === "field" &&
|
|
135
|
+
(body.fn.field === "has" || body.fn.field === "get" || body.fn.field === "includes") &&
|
|
136
|
+
body.args.length === 1 && body.args[0].kind === "var" && body.args[0].name === varName) {
|
|
137
|
+
const objTy = lookup(ctx.env, body.fn.obj.kind === "var" ? body.fn.obj.name : "");
|
|
138
|
+
if (objTy?.kind === "map")
|
|
139
|
+
return objTy.key;
|
|
140
|
+
if (objTy?.kind === "set")
|
|
141
|
+
return objTy.elem;
|
|
142
|
+
if (objTy?.kind === "array")
|
|
143
|
+
return objTy.elem;
|
|
144
|
+
}
|
|
145
|
+
// Recurse into subexpressions
|
|
146
|
+
if (body.kind === "binop") {
|
|
147
|
+
return inferQuantVarType(varName, body.left, ctx) ?? inferQuantVarType(varName, body.right, ctx);
|
|
148
|
+
}
|
|
149
|
+
if (body.kind === "unop")
|
|
150
|
+
return inferQuantVarType(varName, body.expr, ctx);
|
|
151
|
+
if (body.kind === "call") {
|
|
152
|
+
for (const a of body.args) {
|
|
153
|
+
const r = inferQuantVarType(varName, a, ctx);
|
|
154
|
+
if (r)
|
|
155
|
+
return r;
|
|
156
|
+
}
|
|
157
|
+
return inferQuantVarType(varName, body.fn, ctx);
|
|
158
|
+
}
|
|
159
|
+
if (body.kind === "field")
|
|
160
|
+
return inferQuantVarType(varName, body.obj, ctx);
|
|
161
|
+
if (body.kind === "index") {
|
|
162
|
+
return inferQuantVarType(varName, body.obj, ctx) ?? inferQuantVarType(varName, body.idx, ctx);
|
|
163
|
+
}
|
|
164
|
+
if (body.kind === "conditional") {
|
|
165
|
+
return inferQuantVarType(varName, body.cond, ctx) ??
|
|
166
|
+
inferQuantVarType(varName, body.then, ctx) ?? inferQuantVarType(varName, body.else, ctx);
|
|
167
|
+
}
|
|
168
|
+
if ((body.kind === "forall" || body.kind === "exists") && body.var !== varName) {
|
|
169
|
+
return inferQuantVarType(varName, body.body, ctx);
|
|
170
|
+
}
|
|
171
|
+
if (body.kind === "arrayLiteral") {
|
|
172
|
+
for (const el of body.elems) {
|
|
173
|
+
const r = inferQuantVarType(varName, el, ctx);
|
|
174
|
+
if (r)
|
|
175
|
+
return r;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (body.kind === "record") {
|
|
179
|
+
if (body.spread) {
|
|
180
|
+
const r = inferQuantVarType(varName, body.spread, ctx);
|
|
181
|
+
if (r)
|
|
182
|
+
return r;
|
|
183
|
+
}
|
|
184
|
+
for (const f of body.fields) {
|
|
185
|
+
const r = inferQuantVarType(varName, f.value, ctx);
|
|
186
|
+
if (r)
|
|
187
|
+
return r;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
42
192
|
function classifyCall(fn, ctx) {
|
|
43
193
|
if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Math")
|
|
44
194
|
return "pure";
|
|
@@ -59,35 +209,170 @@ function resolveExpr(e, ctx) {
|
|
|
59
209
|
case "var":
|
|
60
210
|
return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
|
|
61
211
|
case "num":
|
|
212
|
+
if (!Number.isInteger(e.value))
|
|
213
|
+
return { kind: "num", value: e.value, ty: { kind: "real" } };
|
|
62
214
|
return { kind: "num", value: e.value, ty: e.value >= 0 ? { kind: "nat" } : { kind: "int" } };
|
|
63
215
|
case "str":
|
|
64
216
|
return { kind: "str", value: e.value, ty: { kind: "string" } };
|
|
65
217
|
case "bool":
|
|
66
218
|
return { kind: "bool", value: e.value, ty: { kind: "bool" } };
|
|
219
|
+
case "nonNull": {
|
|
220
|
+
const expr = resolveExpr(e.expr, ctx);
|
|
221
|
+
// Unwrap optional type; for map.get()!, force to direct access type
|
|
222
|
+
if (expr.kind === "call" && expr.fn.kind === "field" &&
|
|
223
|
+
expr.fn.obj.ty.kind === "map" && expr.fn.field === "get") {
|
|
224
|
+
return { ...expr, ty: expr.fn.obj.ty.value };
|
|
225
|
+
}
|
|
226
|
+
const ty = expr.ty.kind === "optional" ? expr.ty.inner : expr.ty;
|
|
227
|
+
return { ...expr, ty };
|
|
228
|
+
}
|
|
67
229
|
case "binop": {
|
|
68
230
|
let left = resolveExpr(e.left, ctx);
|
|
69
|
-
|
|
231
|
+
// && narrowing: if left is "x !== undefined", narrow x for right side
|
|
232
|
+
let rightCtx = ctx;
|
|
233
|
+
if (e.op === "&&") {
|
|
234
|
+
const narrowed = narrowOptional(e.left, ctx.env);
|
|
235
|
+
if (narrowed && narrowed.inThen) {
|
|
236
|
+
rightCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
let right = resolveExpr(e.right, rightCtx);
|
|
70
240
|
if (e.op === "===" || e.op === "!==") {
|
|
71
241
|
left = coerceStr(left, right.ty);
|
|
72
242
|
right = coerceStr(right, left.ty);
|
|
73
243
|
}
|
|
74
244
|
let ty = { kind: "unknown" };
|
|
75
|
-
if (["===", "!==", ">=", "<=", ">", "<", "
|
|
245
|
+
if (["===", "!==", ">=", "<=", ">", "<", "in"].includes(e.op))
|
|
76
246
|
ty = { kind: "bool" };
|
|
77
|
-
else if (
|
|
78
|
-
ty =
|
|
247
|
+
else if (e.op === "&&")
|
|
248
|
+
ty = right.ty;
|
|
249
|
+
else if (e.op === "||" && left.ty.kind === "optional") {
|
|
250
|
+
// || undefined is identity for optionals — keep the optional type
|
|
251
|
+
ty = (e.right.kind === "var" && e.right.name === "undefined") ? left.ty : left.ty.inner;
|
|
252
|
+
}
|
|
253
|
+
else if (e.op === "||")
|
|
254
|
+
ty = right.ty;
|
|
255
|
+
else if (["+", "-", "*", "/", "%"].includes(e.op)) {
|
|
256
|
+
ty = (left.ty.kind === "real" || right.ty.kind === "real") ? { kind: "real" } : left.ty;
|
|
257
|
+
}
|
|
79
258
|
return { kind: "binop", op: e.op, left, right, ty };
|
|
80
259
|
}
|
|
81
260
|
case "unop": {
|
|
82
261
|
const expr = resolveExpr(e.expr, ctx);
|
|
83
262
|
return { kind: "unop", op: e.op, expr, ty: e.op === "!" ? { kind: "bool" } : expr.ty };
|
|
84
263
|
}
|
|
85
|
-
case "call":
|
|
86
|
-
|
|
264
|
+
case "call": {
|
|
265
|
+
const fn = resolveExpr(e.fn, ctx);
|
|
266
|
+
// Infer lambda param types from array method context (map, filter, etc.)
|
|
267
|
+
let rawArgs = e.args;
|
|
268
|
+
if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
269
|
+
["map", "filter", "every", "some", "find"].includes(fn.field) &&
|
|
270
|
+
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
|
|
271
|
+
rawArgs[0].params.length >= 1 && !rawArgs[0].params[0].tsType) {
|
|
272
|
+
const elemTy = fn.obj.ty.elem;
|
|
273
|
+
const tsType = elemTy.kind === "user" ? elemTy.name
|
|
274
|
+
: elemTy.kind === "string" ? "string"
|
|
275
|
+
: elemTy.kind === "int" || elemTy.kind === "nat" ? "number"
|
|
276
|
+
: elemTy.kind === "bool" ? "boolean" : undefined;
|
|
277
|
+
if (tsType) {
|
|
278
|
+
const lam = rawArgs[0];
|
|
279
|
+
const updatedParams = [{ ...lam.params[0], tsType }, ...lam.params.slice(1)];
|
|
280
|
+
rawArgs = [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// For .push() on a typed array, resolve the argument with element type context
|
|
284
|
+
// so record expressions can match fields and coerce types
|
|
285
|
+
let argCtx = ctx;
|
|
286
|
+
if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "push" &&
|
|
287
|
+
fn.obj.ty.elem.kind === "user") {
|
|
288
|
+
argCtx = { ...ctx, returnTy: fn.obj.ty.elem };
|
|
289
|
+
}
|
|
290
|
+
let args = rawArgs.map(a => resolveExpr(a, argCtx));
|
|
291
|
+
// Coerce args: string literals to user types, non-optional to Option, pad missing optional args
|
|
292
|
+
if (fn.kind === "var" && ctx.fnParams.has(fn.name)) {
|
|
293
|
+
const paramTys = ctx.fnParams.get(fn.name);
|
|
294
|
+
args = args.map((a, i) => {
|
|
295
|
+
if (i >= paramTys.length)
|
|
296
|
+
return a;
|
|
297
|
+
// Coerce string literal to user type (e.g., 'MissingList' → Err constructor)
|
|
298
|
+
a = coerceStr(a, paramTys[i]);
|
|
299
|
+
// Wrap non-optional in Some when callee expects optional param
|
|
300
|
+
if (a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
|
|
301
|
+
return {
|
|
302
|
+
kind: "call",
|
|
303
|
+
fn: { kind: "var", name: "Some", ty: paramTys[i] },
|
|
304
|
+
args: [a],
|
|
305
|
+
ty: paramTys[i],
|
|
306
|
+
callKind: "pure",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return a;
|
|
310
|
+
});
|
|
311
|
+
// Pad missing optional args with None
|
|
312
|
+
for (let i = args.length; i < paramTys.length; i++) {
|
|
313
|
+
if (paramTys[i].kind === "optional") {
|
|
314
|
+
args.push({ kind: "var", name: "undefined", ty: paramTys[i] });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
let ty = { kind: "unknown" };
|
|
319
|
+
// Infer return types for collection methods
|
|
320
|
+
if (fn.kind === "field" && fn.obj.ty.kind === "map") {
|
|
321
|
+
if (fn.field === "get")
|
|
322
|
+
ty = ctx.inSpec ? fn.obj.ty.value : { kind: "optional", inner: fn.obj.ty.value };
|
|
323
|
+
else if (fn.field === "has")
|
|
324
|
+
ty = { kind: "bool" };
|
|
325
|
+
else if (fn.field === "set")
|
|
326
|
+
ty = fn.obj.ty;
|
|
327
|
+
else if (fn.field === "delete")
|
|
328
|
+
ty = fn.obj.ty;
|
|
329
|
+
}
|
|
330
|
+
else if (fn.kind === "field" && fn.obj.ty.kind === "set") {
|
|
331
|
+
if (fn.field === "has")
|
|
332
|
+
ty = { kind: "bool" };
|
|
333
|
+
else if (fn.field === "add")
|
|
334
|
+
ty = fn.obj.ty;
|
|
335
|
+
else if (fn.field === "delete")
|
|
336
|
+
ty = fn.obj.ty;
|
|
337
|
+
}
|
|
338
|
+
else if (fn.kind === "field" && fn.obj.ty.kind === "array") {
|
|
339
|
+
if (fn.field === "includes")
|
|
340
|
+
ty = { kind: "bool" };
|
|
341
|
+
else if (fn.field === "shift")
|
|
342
|
+
ty = fn.obj.ty.elem;
|
|
343
|
+
else if (fn.field === "push")
|
|
344
|
+
ty = fn.obj.ty;
|
|
345
|
+
else if (fn.field === "concat")
|
|
346
|
+
ty = fn.obj.ty;
|
|
347
|
+
else if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
|
|
348
|
+
const retTy = args[0].body.length > 0 && args[0].body[0].kind === "return"
|
|
349
|
+
? args[0].body[0].value.ty : { kind: "unknown" };
|
|
350
|
+
ty = { kind: "array", elem: retTy };
|
|
351
|
+
}
|
|
352
|
+
else if (fn.field === "filter")
|
|
353
|
+
ty = fn.obj.ty;
|
|
354
|
+
else if (fn.field === "every" || fn.field === "some")
|
|
355
|
+
ty = { kind: "bool" };
|
|
356
|
+
}
|
|
357
|
+
else if (fn.kind === "field" && fn.obj.ty.kind === "string") {
|
|
358
|
+
if (fn.field === "trim")
|
|
359
|
+
ty = { kind: "string" };
|
|
360
|
+
else if (fn.field === "toLowerCase")
|
|
361
|
+
ty = { kind: "string" };
|
|
362
|
+
else if (fn.field === "toUpperCase")
|
|
363
|
+
ty = { kind: "string" };
|
|
364
|
+
else if (fn.field === "includes")
|
|
365
|
+
ty = { kind: "bool" };
|
|
366
|
+
}
|
|
367
|
+
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
|
|
368
|
+
}
|
|
87
369
|
case "index": {
|
|
88
370
|
const obj = resolveExpr(e.obj, ctx);
|
|
89
371
|
const idx = resolveExpr(e.idx, ctx);
|
|
90
|
-
|
|
372
|
+
const idxTy = obj.ty.kind === "array" ? obj.ty.elem
|
|
373
|
+
: obj.ty.kind === "map" ? obj.ty.value
|
|
374
|
+
: { kind: "unknown" };
|
|
375
|
+
return { kind: "index", obj, idx, ty: idxTy };
|
|
91
376
|
}
|
|
92
377
|
case "field": {
|
|
93
378
|
const obj = resolveExpr(e.obj, ctx);
|
|
@@ -96,6 +381,9 @@ function resolveExpr(e, ctx) {
|
|
|
96
381
|
if (e.field === "length" && (obj.ty.kind === "array" || obj.ty.kind === "string")) {
|
|
97
382
|
ty = { kind: "nat" };
|
|
98
383
|
}
|
|
384
|
+
else if (e.field === "size" && (obj.ty.kind === "map" || obj.ty.kind === "set")) {
|
|
385
|
+
ty = { kind: "nat" };
|
|
386
|
+
}
|
|
99
387
|
else if (obj.ty.kind === "user") {
|
|
100
388
|
if (getDiscriminant(ctx, obj.ty.name) === e.field)
|
|
101
389
|
isDiscriminant = true;
|
|
@@ -114,11 +402,23 @@ function resolveExpr(e, ctx) {
|
|
|
114
402
|
// Infer record type: from spread, or from return type context
|
|
115
403
|
const recordTy = ty.kind === "user" ? ty : ctx.returnTy.kind === "user" ? ctx.returnTy : null;
|
|
116
404
|
const decl = recordTy ? ctx.typeDecls.find(d => d.name === recordTy.name && d.kind === "record") : undefined;
|
|
405
|
+
// Clear returnTy for field values — it applies to THIS record, not nested ones
|
|
406
|
+
const fieldCtx = recordTy ? { ...ctx, returnTy: { kind: "unknown" } } : ctx;
|
|
117
407
|
const fields = e.fields.map(f => {
|
|
118
|
-
let value = resolveExpr(f.value,
|
|
408
|
+
let value = resolveExpr(f.value, fieldCtx);
|
|
119
409
|
const fieldDecl = decl?.fields?.find(df => df.name === f.name);
|
|
120
|
-
if (fieldDecl)
|
|
121
|
-
|
|
410
|
+
if (fieldDecl) {
|
|
411
|
+
const declTy = parseTsType(fieldDecl.tsType);
|
|
412
|
+
value = coerceStr(value, declTy);
|
|
413
|
+
// Coerce non-optional to optional: wrap in Some (only when value type is concrete)
|
|
414
|
+
if (declTy.kind === "optional" && value.ty.kind !== "optional" && value.ty.kind !== "void" && value.ty.kind !== "unknown") {
|
|
415
|
+
value = {
|
|
416
|
+
kind: "call",
|
|
417
|
+
fn: { kind: "var", name: "Some", ty: declTy },
|
|
418
|
+
args: [value], ty: declTy, callKind: "pure",
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
122
422
|
return { name: f.name, value };
|
|
123
423
|
});
|
|
124
424
|
return { kind: "record", spread, fields, ty: recordTy ?? ty };
|
|
@@ -128,11 +428,13 @@ function resolveExpr(e, ctx) {
|
|
|
128
428
|
throw new Error("\\result is only valid in ensures");
|
|
129
429
|
return { kind: "result", ty: ctx.returnTy };
|
|
130
430
|
case "forall": {
|
|
131
|
-
const varTy = e.varType
|
|
431
|
+
const varTy = e.varType !== "int" ? parseTsType(e.varType)
|
|
432
|
+
: inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
|
|
132
433
|
return { kind: "forall", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
|
|
133
434
|
}
|
|
134
435
|
case "exists": {
|
|
135
|
-
const varTy = e.varType
|
|
436
|
+
const varTy = e.varType !== "int" ? parseTsType(e.varType)
|
|
437
|
+
: inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
|
|
136
438
|
return { kind: "exists", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
|
|
137
439
|
}
|
|
138
440
|
case "arrayLiteral": {
|
|
@@ -159,13 +461,64 @@ function resolveExpr(e, ctx) {
|
|
|
159
461
|
}
|
|
160
462
|
case "conditional": {
|
|
161
463
|
const cond = resolveExpr(e.cond, ctx);
|
|
162
|
-
|
|
464
|
+
// Optional truthiness: opt ? X : Y
|
|
465
|
+
// Narrow the optional to its inner type in the then-branch so that
|
|
466
|
+
// field accesses resolve correctly (e.g. entry.decision.field).
|
|
467
|
+
let narrowedVar;
|
|
468
|
+
let narrowedExprResolved;
|
|
469
|
+
let thenCtx = ctx;
|
|
470
|
+
let rawThen = e.then;
|
|
471
|
+
if (cond.ty.kind === "optional") {
|
|
472
|
+
const innerTy = cond.ty.inner;
|
|
473
|
+
if (e.cond.kind === "var") {
|
|
474
|
+
narrowedVar = e.cond.name;
|
|
475
|
+
thenCtx = withEnv(ctx, extend(ctx.env, e.cond.name, innerTy));
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
narrowedVar = `_opt${_synVarCounter++}`;
|
|
479
|
+
rawThen = substituteRawExpr(e.then, e.cond, { kind: "var", name: narrowedVar });
|
|
480
|
+
thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Explicit optional check: x !== undefined ? expr(x) : undefined
|
|
484
|
+
// Narrow x to its inner type in the then-branch.
|
|
485
|
+
if (!narrowedVar) {
|
|
486
|
+
const narrowed = narrowOptional(e.cond, ctx.env);
|
|
487
|
+
if (narrowed && narrowed.inThen) {
|
|
488
|
+
narrowedVar = narrowed.varName;
|
|
489
|
+
thenCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
|
|
490
|
+
}
|
|
491
|
+
// Handle complex optional expressions: f() !== undefined ? f().field : undefined
|
|
492
|
+
if (!narrowedVar && e.cond.kind === "binop" && e.cond.op === "!==" &&
|
|
493
|
+
e.cond.right.kind === "var" && e.cond.right.name === "undefined") {
|
|
494
|
+
const optExpr = e.cond.left;
|
|
495
|
+
const resolvedOpt = resolveExpr(optExpr, ctx);
|
|
496
|
+
if (resolvedOpt.ty.kind === "optional") {
|
|
497
|
+
narrowedVar = `_opt${_synVarCounter++}`;
|
|
498
|
+
narrowedExprResolved = resolvedOpt;
|
|
499
|
+
rawThen = substituteRawExpr(e.then, optExpr, { kind: "var", name: narrowedVar });
|
|
500
|
+
thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, resolvedOpt.ty.inner));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
let then_ = resolveExpr(rawThen, thenCtx);
|
|
163
505
|
let else_ = resolveExpr(e.else, ctx);
|
|
164
506
|
then_ = coerceStr(then_, else_.ty);
|
|
165
507
|
else_ = coerceStr(else_, then_.ty);
|
|
166
|
-
|
|
167
|
-
|
|
508
|
+
let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
|
|
509
|
+
// When narrowedExpr is set, the transform will emit a match producing Optional
|
|
510
|
+
if (narrowedExprResolved && ty.kind !== "optional") {
|
|
511
|
+
ty = { kind: "optional", inner: ty };
|
|
512
|
+
}
|
|
513
|
+
return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
|
|
168
514
|
}
|
|
515
|
+
case "emptyCollection": {
|
|
516
|
+
const ty = parseTsType(e.tsType);
|
|
517
|
+
const elems = e.initElems ? e.initElems.map(el => resolveExpr(el, ctx)) : [];
|
|
518
|
+
return { kind: "arrayLiteral", elems, ty };
|
|
519
|
+
}
|
|
520
|
+
case "havoc":
|
|
521
|
+
return { kind: "havoc", ty: resolveTsType(e.tsType, ctx.overrides) };
|
|
169
522
|
}
|
|
170
523
|
}
|
|
171
524
|
// ── Resolve specs ────────────────────────────────────────────
|
|
@@ -202,22 +555,54 @@ function resolveStmt(s, ctx) {
|
|
|
202
555
|
case "let": {
|
|
203
556
|
const ty = resolveTsType(s.tsType, ctx.overrides, s.name);
|
|
204
557
|
const init = coerceStr(resolveExpr(s.init, ctx), ty);
|
|
205
|
-
|
|
558
|
+
// const collections are mutable in value-semantics world (TS mutates in place, Dafny/Lean reassign)
|
|
559
|
+
const mutable = s.mutable || isRefMutableInTS(ty);
|
|
560
|
+
return [{ kind: "let", name: s.name, ty, mutable, init }, extend(ctx.env, s.name, ty)];
|
|
206
561
|
}
|
|
207
562
|
case "assign": {
|
|
208
563
|
const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
|
|
209
564
|
return [{ kind: "assign", target: s.target, value: coerceStr(resolveExpr(s.value, ctx), targetTy) }, ctx.env];
|
|
210
565
|
}
|
|
211
|
-
case "return":
|
|
212
|
-
|
|
566
|
+
case "return": {
|
|
567
|
+
let value = coerceStr(resolveExpr(s.value, ctx), ctx.returnTy);
|
|
568
|
+
// Wrap non-optional return value in Some when function returns optional
|
|
569
|
+
// Skip if already optional, void, or undefined (which maps to None)
|
|
570
|
+
const isUndef = value.kind === "var" && value.name === "undefined";
|
|
571
|
+
if (ctx.returnTy.kind === "optional" && value.ty.kind !== "optional" && !isUndef) {
|
|
572
|
+
value = {
|
|
573
|
+
kind: "call",
|
|
574
|
+
fn: { kind: "var", name: "Some", ty: ctx.returnTy },
|
|
575
|
+
args: [value], ty: ctx.returnTy, callKind: "pure",
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return [{ kind: "return", value }, ctx.env];
|
|
579
|
+
}
|
|
213
580
|
case "break":
|
|
214
581
|
return [{ kind: "break" }, ctx.env];
|
|
215
582
|
case "continue":
|
|
216
583
|
return [{ kind: "continue" }, ctx.env];
|
|
217
584
|
case "expr":
|
|
218
585
|
return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
|
|
219
|
-
case "if":
|
|
220
|
-
|
|
586
|
+
case "if": {
|
|
587
|
+
// Narrow optional<T> → T when checking !== undefined or undefined !==
|
|
588
|
+
let thenCtx = ctx, elseCtx = ctx;
|
|
589
|
+
const narrowed = narrowOptional(s.cond, ctx.env);
|
|
590
|
+
if (narrowed) {
|
|
591
|
+
const env = extend(ctx.env, narrowed.varName, narrowed.innerTy);
|
|
592
|
+
if (narrowed.inThen)
|
|
593
|
+
thenCtx = withEnv(ctx, env);
|
|
594
|
+
else
|
|
595
|
+
elseCtx = withEnv(ctx, env);
|
|
596
|
+
}
|
|
597
|
+
// Also narrow from left side of && condition: if (x !== undefined && ...) { ... }
|
|
598
|
+
if (!narrowed && s.cond.kind === "binop" && s.cond.op === "&&") {
|
|
599
|
+
const leftNarrowed = narrowOptional(s.cond.left, ctx.env);
|
|
600
|
+
if (leftNarrowed && leftNarrowed.inThen) {
|
|
601
|
+
thenCtx = withEnv(ctx, extend(ctx.env, leftNarrowed.varName, leftNarrowed.innerTy));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return [{ kind: "if", cond: resolveExpr(s.cond, ctx), then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
|
|
605
|
+
}
|
|
221
606
|
case "while": {
|
|
222
607
|
const whileSpecCtx = { ...ctx, inSpec: true };
|
|
223
608
|
return [{
|
|
@@ -231,24 +616,66 @@ function resolveStmt(s, ctx) {
|
|
|
231
616
|
}
|
|
232
617
|
case "forof": {
|
|
233
618
|
const iterable = resolveExpr(s.iterable, ctx);
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
619
|
+
// Determine element types for each destructured name
|
|
620
|
+
const nameTypes = [];
|
|
621
|
+
let env = ctx.env;
|
|
622
|
+
if (s.names.length === 1) {
|
|
623
|
+
// Single name: element type from array/set
|
|
624
|
+
const elemTy = iterable.ty.kind === "array" ? iterable.ty.elem
|
|
625
|
+
: iterable.ty.kind === "set" ? iterable.ty.elem
|
|
626
|
+
: { kind: "unknown" };
|
|
627
|
+
nameTypes.push(elemTy);
|
|
628
|
+
}
|
|
629
|
+
else if (s.names.length >= 2 && iterable.ty.kind === "map") {
|
|
630
|
+
// Map destructuring: [key, value]
|
|
631
|
+
nameTypes.push(iterable.ty.key, iterable.ty.value);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
// General tuple destructuring: all unknown
|
|
635
|
+
for (const _ of s.names)
|
|
636
|
+
nameTypes.push({ kind: "unknown" });
|
|
637
|
+
}
|
|
638
|
+
const idxName = `_${s.names[0]}_idx`;
|
|
639
|
+
env = extend(env, idxName, { kind: "nat" });
|
|
640
|
+
for (let j = 0; j < s.names.length; j++) {
|
|
641
|
+
env = extend(env, s.names[j], nameTypes[j] ?? { kind: "unknown" });
|
|
642
|
+
}
|
|
643
|
+
const bodyCtx = withEnv(ctx, env);
|
|
239
644
|
return [{
|
|
240
|
-
kind: "forof",
|
|
645
|
+
kind: "forof", names: s.names, nameTypes, iterable,
|
|
241
646
|
invariants: resolveSpecs(s.invariants, { ...bodyCtx, inSpec: true }),
|
|
242
647
|
doneWith: s.doneWith ? resolveSpec(s.doneWith, { ...bodyCtx, inSpec: true }) : null,
|
|
243
648
|
body: resolveBlock(s.body, bodyCtx),
|
|
244
649
|
}, ctx.env];
|
|
245
650
|
}
|
|
651
|
+
case "throw":
|
|
652
|
+
return [{ kind: "throw" }, ctx.env];
|
|
246
653
|
case "switch":
|
|
247
654
|
return [{
|
|
248
655
|
kind: "switch", expr: resolveExpr(s.expr, ctx), discriminant: s.discriminant,
|
|
249
656
|
cases: s.cases.map(c => ({ label: c.label, body: resolveBlock(c.body, ctx) })),
|
|
250
657
|
defaultBody: resolveBlock(s.defaultBody, ctx),
|
|
251
658
|
}, ctx.env];
|
|
659
|
+
case "ghostLet": {
|
|
660
|
+
const specCtx = { ...ctx, inSpec: true };
|
|
661
|
+
// Handle new Set<T>() / new Map<K,V>() constructors
|
|
662
|
+
const collMatch = s.init.match(/^new\s+(Set|Map)<(.+)>\(\)$/);
|
|
663
|
+
const init = collMatch
|
|
664
|
+
? resolveExpr({ kind: "emptyCollection", collectionType: collMatch[1], tsType: `${collMatch[1]}<${collMatch[2]}>` }, specCtx)
|
|
665
|
+
: resolveExpr(parseExpr(s.init), specCtx);
|
|
666
|
+
const ty = s.tsType ? parseTsType(s.tsType) : init.ty;
|
|
667
|
+
return [{ kind: "ghostLet", name: s.name, ty, init }, extend(ctx.env, s.name, ty)];
|
|
668
|
+
}
|
|
669
|
+
case "ghostAssign": {
|
|
670
|
+
const specCtx = { ...ctx, inSpec: true };
|
|
671
|
+
const value = resolveExpr(parseExpr(s.value), specCtx);
|
|
672
|
+
return [{ kind: "ghostAssign", target: s.target, value }, ctx.env];
|
|
673
|
+
}
|
|
674
|
+
case "assert": {
|
|
675
|
+
const specCtx = { ...ctx, inSpec: true };
|
|
676
|
+
const expr = resolveExpr(parseExpr(s.expr), specCtx);
|
|
677
|
+
return [{ kind: "assert", expr }, ctx.env];
|
|
678
|
+
}
|
|
252
679
|
}
|
|
253
680
|
}
|
|
254
681
|
// ── Pure / return-in-loop detection ──────────────────────────
|
|
@@ -259,7 +686,7 @@ function isSyntacticallyPure(stmts) {
|
|
|
259
686
|
case "while":
|
|
260
687
|
case "forof": return false;
|
|
261
688
|
case "let":
|
|
262
|
-
if (s.mutable)
|
|
689
|
+
if (s.mutable || s.init.kind === "havoc")
|
|
263
690
|
return false;
|
|
264
691
|
break;
|
|
265
692
|
case "if":
|
|
@@ -420,32 +847,71 @@ function containsReturn(stmts) {
|
|
|
420
847
|
return false;
|
|
421
848
|
}
|
|
422
849
|
// ── Resolve function / module ────────────────────────────────
|
|
423
|
-
function resolveFunction(fn, typeDecls, pureFns) {
|
|
424
|
-
if (hasReturnInLoop(fn.body)) {
|
|
425
|
-
throw new Error(`${fn.name}: return inside a loop is not supported.`);
|
|
426
|
-
}
|
|
850
|
+
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map()) {
|
|
427
851
|
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
428
852
|
const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
|
|
429
853
|
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
430
854
|
let env = null;
|
|
431
855
|
for (const p of params)
|
|
432
856
|
env = extend(env, p.name, p.ty);
|
|
433
|
-
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, inSpec: false, inLambda: false };
|
|
857
|
+
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
|
|
434
858
|
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
435
859
|
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
436
860
|
return {
|
|
437
|
-
name: fn.name, params, returnTy,
|
|
861
|
+
name: fn.name, typeParams: fn.typeParams, params, returnTy,
|
|
438
862
|
requires: resolveSpecs(fn.requires, requiresCtx),
|
|
439
863
|
ensures: resolveSpecs(fn.ensures, ensuresCtx),
|
|
440
864
|
isPure: pureFns.has(fn.name),
|
|
441
865
|
body: resolveBlock(fn.body, baseCtx),
|
|
442
866
|
};
|
|
443
867
|
}
|
|
868
|
+
function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
|
|
869
|
+
const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
|
|
870
|
+
// Create a synthetic record type for 'this' so field access resolves
|
|
871
|
+
const thisType = { kind: "user", name: cls.name };
|
|
872
|
+
const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType })) };
|
|
873
|
+
const allTypeDecls = [...typeDecls, thisDecl];
|
|
874
|
+
const methods = cls.methods.map(fn => {
|
|
875
|
+
// Add 'this' to the environment
|
|
876
|
+
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
877
|
+
const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
|
|
878
|
+
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
879
|
+
let env = null;
|
|
880
|
+
env = extend(env, "this", thisType);
|
|
881
|
+
for (const p of params)
|
|
882
|
+
env = extend(env, p.name, p.ty);
|
|
883
|
+
const baseCtx = { env, typeDecls: allTypeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
|
|
884
|
+
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
885
|
+
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
886
|
+
return {
|
|
887
|
+
name: fn.name, typeParams: fn.typeParams, params, returnTy,
|
|
888
|
+
requires: resolveSpecs(fn.requires, requiresCtx),
|
|
889
|
+
ensures: resolveSpecs(fn.ensures, ensuresCtx),
|
|
890
|
+
isPure: false, // class methods are never pure (they access this)
|
|
891
|
+
body: resolveBlock(fn.body, baseCtx),
|
|
892
|
+
};
|
|
893
|
+
});
|
|
894
|
+
return { name: cls.name, fields, methods };
|
|
895
|
+
}
|
|
444
896
|
export function resolveModule(raw) {
|
|
445
897
|
const pureFns = computePureFns(raw.functions);
|
|
898
|
+
// Pre-compute function parameter types for optional coercion
|
|
899
|
+
const fnParams = new Map();
|
|
900
|
+
for (const fn of raw.functions) {
|
|
901
|
+
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
902
|
+
fnParams.set(fn.name, fn.params.map(p => resolveTsType(p.tsType, overrides, p.name)));
|
|
903
|
+
}
|
|
904
|
+
const emptyCtx = { env: null, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: { kind: "int" }, pureFns, fnParams, inSpec: false, inLambda: false };
|
|
905
|
+
const constants = (raw.constants ?? []).map(c => ({
|
|
906
|
+
name: c.name,
|
|
907
|
+
ty: parseTsType(c.tsType),
|
|
908
|
+
value: resolveExpr(c.value, emptyCtx),
|
|
909
|
+
}));
|
|
446
910
|
return {
|
|
447
911
|
file: raw.file,
|
|
448
912
|
typeDecls: raw.typeDecls,
|
|
449
|
-
|
|
913
|
+
constants,
|
|
914
|
+
functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns, fnParams)),
|
|
915
|
+
classes: (raw.classes ?? []).map(cls => resolveClass(cls, raw.typeDecls, pureFns, fnParams)),
|
|
450
916
|
};
|
|
451
917
|
}
|