lemmascript 0.2.0 → 0.3.1
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 +3 -2
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +4 -3
- package/tools/dist/dafny-emit.js +206 -161
- package/tools/dist/extract.js +483 -51
- package/tools/dist/lean-emit.js +6 -2
- package/tools/dist/lsc.js +28 -5
- package/tools/dist/resolve.js +353 -113
- package/tools/dist/specparser.js +5 -2
- package/tools/dist/transform.js +452 -117
- package/tools/dist/types.js +14 -1
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,22 +99,47 @@ function coerceStr(expr, targetTy) {
|
|
|
33
99
|
return expr;
|
|
34
100
|
}
|
|
35
101
|
// ── Helpers ──────────────────────────────────────────────────
|
|
36
|
-
/**
|
|
37
|
-
function
|
|
102
|
+
/** Wrap a resolved expression in Some() for optional coercion. */
|
|
103
|
+
function wrapSome(value, optionalTy) {
|
|
104
|
+
return {
|
|
105
|
+
kind: "call", fn: { kind: "var", name: "Some", ty: optionalTy },
|
|
106
|
+
args: [value], ty: optionalTy, callKind: "pure",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Detect `v !== undefined` or `undefined !== v` where v: optional<T>.
|
|
110
|
+
* Handles simple variables, field access chains, and arbitrary expressions.
|
|
111
|
+
* When `fieldExpr` is returned, callers must use `substituteRawExpr` to narrow.
|
|
112
|
+
*
|
|
113
|
+
* Does NOT recurse into `&&` — callers that need to detect optional checks
|
|
114
|
+
* inside `&&` conditions should check `cond.left` explicitly. */
|
|
115
|
+
function detectOptionalCheck(cond, ctx) {
|
|
38
116
|
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
|
|
39
117
|
return null;
|
|
40
|
-
//
|
|
41
|
-
let
|
|
42
|
-
if (cond.
|
|
43
|
-
|
|
44
|
-
if (cond.
|
|
45
|
-
|
|
46
|
-
if (!
|
|
47
|
-
return null;
|
|
48
|
-
const ty = lookup(env, varName);
|
|
49
|
-
if (!ty || ty.kind !== "optional")
|
|
118
|
+
// Identify the expression being checked against undefined
|
|
119
|
+
let optExpr = null;
|
|
120
|
+
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
121
|
+
optExpr = cond.left;
|
|
122
|
+
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
123
|
+
optExpr = cond.right;
|
|
124
|
+
if (!optExpr)
|
|
50
125
|
return null;
|
|
51
|
-
|
|
126
|
+
// Simple variable — env lookup, no substitution needed
|
|
127
|
+
if (optExpr.kind === "var") {
|
|
128
|
+
const ty = lookup(ctx.env, optExpr.name);
|
|
129
|
+
if (!ty || ty.kind !== "optional")
|
|
130
|
+
return null;
|
|
131
|
+
return { varName: optExpr.name, innerTy: ty.inner, inThen: cond.op === "!==" };
|
|
132
|
+
}
|
|
133
|
+
// Field access chain or arbitrary expression — resolve to check type, needs substitution
|
|
134
|
+
const resolved = resolveExpr(optExpr, ctx);
|
|
135
|
+
if (resolved.ty.kind === "optional") {
|
|
136
|
+
const synVar = optExpr.kind === "field" ? `_narr${_synVarCounter++}` : `_opt${_synVarCounter++}`;
|
|
137
|
+
return {
|
|
138
|
+
varName: synVar, innerTy: resolved.ty.inner, inThen: cond.op === "!==",
|
|
139
|
+
fieldExpr: optExpr, narrowedExpr: resolved,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
52
143
|
}
|
|
53
144
|
/** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
|
|
54
145
|
function isRefMutableInTS(ty) {
|
|
@@ -137,10 +228,99 @@ function classifyCall(fn, ctx) {
|
|
|
137
228
|
return "method";
|
|
138
229
|
return "unknown";
|
|
139
230
|
}
|
|
231
|
+
// ── Call resolution helpers ─────────────────────────────────
|
|
232
|
+
/** Infer lambda param types from array method context (map, filter, etc.).
|
|
233
|
+
* Returns updated rawArgs with inferred tsType on the first lambda param. */
|
|
234
|
+
function inferLambdaParamTypes(fn, rawArgs) {
|
|
235
|
+
if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
236
|
+
["map", "filter", "every", "some", "find"].includes(fn.field) &&
|
|
237
|
+
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
|
|
238
|
+
rawArgs[0].params.length >= 1 && !rawArgs[0].params[0].tsType) {
|
|
239
|
+
const elemTy = fn.obj.ty.elem;
|
|
240
|
+
const tsType = elemTy.kind === "user" ? elemTy.name
|
|
241
|
+
: elemTy.kind === "string" ? "string"
|
|
242
|
+
: elemTy.kind === "int" || elemTy.kind === "nat" ? "number"
|
|
243
|
+
: elemTy.kind === "bool" ? "boolean" : undefined;
|
|
244
|
+
if (tsType) {
|
|
245
|
+
const lam = rawArgs[0];
|
|
246
|
+
const updatedParams = [{ ...lam.params[0], tsType }, ...lam.params.slice(1)];
|
|
247
|
+
return [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return rawArgs;
|
|
251
|
+
}
|
|
252
|
+
/** Coerce call arguments: string literals → user types, non-optional → Some, pad missing optional args. */
|
|
253
|
+
function coerceCallArgs(args, fn, ctx) {
|
|
254
|
+
if (fn.kind !== "var" || !ctx.fnParams.has(fn.name))
|
|
255
|
+
return args;
|
|
256
|
+
const paramTys = ctx.fnParams.get(fn.name);
|
|
257
|
+
args = args.map((a, i) => {
|
|
258
|
+
if (i >= paramTys.length)
|
|
259
|
+
return a;
|
|
260
|
+
a = coerceStr(a, paramTys[i]);
|
|
261
|
+
if (a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
|
|
262
|
+
return wrapSome(a, paramTys[i]);
|
|
263
|
+
}
|
|
264
|
+
return a;
|
|
265
|
+
});
|
|
266
|
+
// Pad missing optional args with None
|
|
267
|
+
for (let i = args.length; i < paramTys.length; i++) {
|
|
268
|
+
if (paramTys[i].kind === "optional") {
|
|
269
|
+
args.push({ kind: "var", name: "undefined", ty: paramTys[i] });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return args;
|
|
273
|
+
}
|
|
274
|
+
/** Infer return type for collection/string method calls. */
|
|
275
|
+
function inferMethodReturnTy(fn, args, ctx) {
|
|
276
|
+
if (fn.kind !== "field")
|
|
277
|
+
return { kind: "unknown" };
|
|
278
|
+
const objTy = fn.obj.ty;
|
|
279
|
+
if (objTy.kind === "map") {
|
|
280
|
+
if (fn.field === "get")
|
|
281
|
+
return ctx.inSpec ? objTy.value : { kind: "optional", inner: objTy.value };
|
|
282
|
+
if (fn.field === "has")
|
|
283
|
+
return { kind: "bool" };
|
|
284
|
+
if (fn.field === "set" || fn.field === "delete")
|
|
285
|
+
return objTy;
|
|
286
|
+
}
|
|
287
|
+
else if (objTy.kind === "set") {
|
|
288
|
+
if (fn.field === "has")
|
|
289
|
+
return { kind: "bool" };
|
|
290
|
+
if (fn.field === "add" || fn.field === "delete")
|
|
291
|
+
return objTy;
|
|
292
|
+
}
|
|
293
|
+
else if (objTy.kind === "array") {
|
|
294
|
+
if (fn.field === "includes")
|
|
295
|
+
return { kind: "bool" };
|
|
296
|
+
if (fn.field === "shift")
|
|
297
|
+
return objTy.elem;
|
|
298
|
+
if (fn.field === "push" || fn.field === "concat")
|
|
299
|
+
return objTy;
|
|
300
|
+
if (fn.field === "filter")
|
|
301
|
+
return objTy;
|
|
302
|
+
if (fn.field === "every" || fn.field === "some")
|
|
303
|
+
return { kind: "bool" };
|
|
304
|
+
if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
|
|
305
|
+
const retTy = args[0].body.length > 0 && args[0].body[0].kind === "return"
|
|
306
|
+
? args[0].body[0].value.ty : { kind: "unknown" };
|
|
307
|
+
return { kind: "array", elem: retTy };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else if (objTy.kind === "string") {
|
|
311
|
+
if (fn.field === "trim" || fn.field === "toLowerCase" || fn.field === "toUpperCase")
|
|
312
|
+
return { kind: "string" };
|
|
313
|
+
if (fn.field === "includes")
|
|
314
|
+
return { kind: "bool" };
|
|
315
|
+
}
|
|
316
|
+
return { kind: "unknown" };
|
|
317
|
+
}
|
|
140
318
|
// ── Resolve expressions ──────────────────────────────────────
|
|
141
319
|
function resolveExpr(e, ctx) {
|
|
142
320
|
switch (e.kind) {
|
|
143
321
|
case "var":
|
|
322
|
+
if (e.name === "undefined")
|
|
323
|
+
return { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
144
324
|
return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
|
|
145
325
|
case "num":
|
|
146
326
|
if (!Number.isInteger(e.value))
|
|
@@ -162,18 +342,31 @@ function resolveExpr(e, ctx) {
|
|
|
162
342
|
}
|
|
163
343
|
case "binop": {
|
|
164
344
|
let left = resolveExpr(e.left, ctx);
|
|
165
|
-
|
|
345
|
+
// && narrowing: if left is "x !== undefined", narrow x for right side.
|
|
346
|
+
// Field chains are excluded — resolve can't substitute in sub-expressions;
|
|
347
|
+
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
348
|
+
let rightCtx = ctx;
|
|
349
|
+
let rawRight = e.right;
|
|
350
|
+
if (e.op === "&&") {
|
|
351
|
+
const narrowed = detectOptionalCheck(e.left, ctx);
|
|
352
|
+
if (narrowed && narrowed.inThen && !narrowed.fieldExpr) {
|
|
353
|
+
rightCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
let right = resolveExpr(rawRight, rightCtx);
|
|
166
357
|
if (e.op === "===" || e.op === "!==") {
|
|
167
358
|
left = coerceStr(left, right.ty);
|
|
168
359
|
right = coerceStr(right, left.ty);
|
|
169
360
|
}
|
|
170
361
|
let ty = { kind: "unknown" };
|
|
171
|
-
if (["===", "!==", ">=", "<=", ">", "<"].includes(e.op))
|
|
362
|
+
if (["===", "!==", ">=", "<=", ">", "<", "in"].includes(e.op))
|
|
172
363
|
ty = { kind: "bool" };
|
|
173
364
|
else if (e.op === "&&")
|
|
174
365
|
ty = right.ty;
|
|
175
|
-
else if (e.op === "||" && left.ty.kind === "optional")
|
|
176
|
-
|
|
366
|
+
else if (e.op === "||" && left.ty.kind === "optional") {
|
|
367
|
+
// || undefined is identity for optionals — keep the optional type
|
|
368
|
+
ty = (e.right.kind === "var" && e.right.name === "undefined") ? left.ty : left.ty.inner;
|
|
369
|
+
}
|
|
177
370
|
else if (e.op === "||")
|
|
178
371
|
ty = right.ty;
|
|
179
372
|
else if (["+", "-", "*", "/", "%"].includes(e.op)) {
|
|
@@ -187,65 +380,24 @@ function resolveExpr(e, ctx) {
|
|
|
187
380
|
}
|
|
188
381
|
case "call": {
|
|
189
382
|
const fn = resolveExpr(e.fn, ctx);
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
kind: "call",
|
|
198
|
-
fn: { kind: "var", name: "Some", ty: paramTys[i] },
|
|
199
|
-
args: [a],
|
|
200
|
-
ty: paramTys[i],
|
|
201
|
-
callKind: "pure",
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
return a;
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
let ty = { kind: "unknown" };
|
|
208
|
-
// Infer return types for collection methods
|
|
209
|
-
if (fn.kind === "field" && fn.obj.ty.kind === "map") {
|
|
210
|
-
if (fn.field === "get")
|
|
211
|
-
ty = ctx.inSpec ? fn.obj.ty.value : { kind: "optional", inner: fn.obj.ty.value };
|
|
212
|
-
else if (fn.field === "has")
|
|
213
|
-
ty = { kind: "bool" };
|
|
214
|
-
else if (fn.field === "set")
|
|
215
|
-
ty = fn.obj.ty;
|
|
216
|
-
}
|
|
217
|
-
else if (fn.kind === "field" && fn.obj.ty.kind === "set") {
|
|
218
|
-
if (fn.field === "has")
|
|
219
|
-
ty = { kind: "bool" };
|
|
220
|
-
else if (fn.field === "add")
|
|
221
|
-
ty = fn.obj.ty;
|
|
222
|
-
else if (fn.field === "delete")
|
|
223
|
-
ty = fn.obj.ty;
|
|
224
|
-
}
|
|
225
|
-
else if (fn.kind === "field" && fn.obj.ty.kind === "array") {
|
|
226
|
-
if (fn.field === "includes")
|
|
227
|
-
ty = { kind: "bool" };
|
|
228
|
-
else if (fn.field === "shift")
|
|
229
|
-
ty = fn.obj.ty.elem;
|
|
230
|
-
else if (fn.field === "push")
|
|
231
|
-
ty = fn.obj.ty;
|
|
232
|
-
}
|
|
233
|
-
else if (fn.kind === "field" && fn.obj.ty.kind === "string") {
|
|
234
|
-
if (fn.field === "trim")
|
|
235
|
-
ty = { kind: "string" };
|
|
236
|
-
else if (fn.field === "toLowerCase")
|
|
237
|
-
ty = { kind: "string" };
|
|
238
|
-
else if (fn.field === "toUpperCase")
|
|
239
|
-
ty = { kind: "string" };
|
|
240
|
-
else if (fn.field === "includes")
|
|
241
|
-
ty = { kind: "bool" };
|
|
383
|
+
const rawArgs = inferLambdaParamTypes(fn, e.args);
|
|
384
|
+
// For .push() on a typed array, resolve args with element type context
|
|
385
|
+
let argCtx = ctx;
|
|
386
|
+
if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "push" &&
|
|
387
|
+
fn.obj.ty.elem.kind === "user") {
|
|
388
|
+
argCtx = { ...ctx, returnTy: fn.obj.ty.elem };
|
|
242
389
|
}
|
|
390
|
+
const args = coerceCallArgs(rawArgs.map(a => resolveExpr(a, argCtx)), fn, ctx);
|
|
391
|
+
const ty = inferMethodReturnTy(fn, args, ctx);
|
|
243
392
|
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
|
|
244
393
|
}
|
|
245
394
|
case "index": {
|
|
246
395
|
const obj = resolveExpr(e.obj, ctx);
|
|
247
396
|
const idx = resolveExpr(e.idx, ctx);
|
|
248
|
-
|
|
397
|
+
const idxTy = obj.ty.kind === "array" ? obj.ty.elem
|
|
398
|
+
: obj.ty.kind === "map" ? obj.ty.value
|
|
399
|
+
: { kind: "unknown" };
|
|
400
|
+
return { kind: "index", obj, idx, ty: idxTy };
|
|
249
401
|
}
|
|
250
402
|
case "field": {
|
|
251
403
|
const obj = resolveExpr(e.obj, ctx);
|
|
@@ -264,7 +416,17 @@ function resolveExpr(e, ctx) {
|
|
|
264
416
|
if (decl?.kind === "record") {
|
|
265
417
|
const f = decl.fields?.find(f => f.name === e.field);
|
|
266
418
|
if (f)
|
|
267
|
-
ty =
|
|
419
|
+
ty = f.type;
|
|
420
|
+
}
|
|
421
|
+
// Also resolve fields from discriminated-union variants
|
|
422
|
+
if (ty.kind === "unknown" && decl?.kind === "discriminated-union" && decl.variants) {
|
|
423
|
+
for (const variant of decl.variants) {
|
|
424
|
+
const f = variant.fields.find(f => f.name === e.field);
|
|
425
|
+
if (f) {
|
|
426
|
+
ty = f.type;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
268
430
|
}
|
|
269
431
|
}
|
|
270
432
|
return { kind: "field", obj, field: e.field, ty, isDiscriminant };
|
|
@@ -275,11 +437,19 @@ function resolveExpr(e, ctx) {
|
|
|
275
437
|
// Infer record type: from spread, or from return type context
|
|
276
438
|
const recordTy = ty.kind === "user" ? ty : ctx.returnTy.kind === "user" ? ctx.returnTy : null;
|
|
277
439
|
const decl = recordTy ? ctx.typeDecls.find(d => d.name === recordTy.name && d.kind === "record") : undefined;
|
|
440
|
+
// Clear returnTy for field values — it applies to THIS record, not nested ones
|
|
441
|
+
const fieldCtx = recordTy ? { ...ctx, returnTy: { kind: "unknown" } } : ctx;
|
|
278
442
|
const fields = e.fields.map(f => {
|
|
279
|
-
let value = resolveExpr(f.value,
|
|
443
|
+
let value = resolveExpr(f.value, fieldCtx);
|
|
280
444
|
const fieldDecl = decl?.fields?.find(df => df.name === f.name);
|
|
281
|
-
if (fieldDecl)
|
|
282
|
-
|
|
445
|
+
if (fieldDecl) {
|
|
446
|
+
const declTy = fieldDecl.type;
|
|
447
|
+
value = coerceStr(value, declTy);
|
|
448
|
+
// Coerce non-optional to optional: wrap in Some (only when value type is concrete)
|
|
449
|
+
if (declTy.kind === "optional" && value.ty.kind !== "optional" && value.ty.kind !== "void" && value.ty.kind !== "unknown") {
|
|
450
|
+
value = wrapSome(value, declTy);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
283
453
|
return { name: f.name, value };
|
|
284
454
|
});
|
|
285
455
|
return { kind: "record", spread, fields, ty: recordTy ?? ty };
|
|
@@ -289,12 +459,12 @@ function resolveExpr(e, ctx) {
|
|
|
289
459
|
throw new Error("\\result is only valid in ensures");
|
|
290
460
|
return { kind: "result", ty: ctx.returnTy };
|
|
291
461
|
case "forall": {
|
|
292
|
-
const varTy = e.varType
|
|
462
|
+
const varTy = e.varType !== "int" ? parseTsType(e.varType)
|
|
293
463
|
: inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
|
|
294
464
|
return { kind: "forall", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
|
|
295
465
|
}
|
|
296
466
|
case "exists": {
|
|
297
|
-
const varTy = e.varType
|
|
467
|
+
const varTy = e.varType !== "int" ? parseTsType(e.varType)
|
|
298
468
|
: inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
|
|
299
469
|
return { kind: "exists", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
|
|
300
470
|
}
|
|
@@ -322,16 +492,61 @@ function resolveExpr(e, ctx) {
|
|
|
322
492
|
}
|
|
323
493
|
case "conditional": {
|
|
324
494
|
const cond = resolveExpr(e.cond, ctx);
|
|
325
|
-
let
|
|
495
|
+
let narrowedVar;
|
|
496
|
+
let narrowedExprResolved;
|
|
497
|
+
let thenCtx = ctx;
|
|
498
|
+
let rawThen = e.then;
|
|
499
|
+
// Phase 1: Optional truthiness — cond itself is optional (e.g. opt ? X : Y)
|
|
500
|
+
if (cond.ty.kind === "optional") {
|
|
501
|
+
const innerTy = cond.ty.inner;
|
|
502
|
+
if (e.cond.kind === "var") {
|
|
503
|
+
narrowedVar = e.cond.name;
|
|
504
|
+
thenCtx = withEnv(ctx, extend(ctx.env, e.cond.name, innerTy));
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
narrowedVar = `_opt${_synVarCounter++}`;
|
|
508
|
+
rawThen = substituteRawExpr(e.then, e.cond, { kind: "var", name: narrowedVar });
|
|
509
|
+
thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// Phase 2: Explicit check — v !== undefined (simple vars, field chains,
|
|
513
|
+
// complex expressions all handled uniformly by detectOptionalCheck)
|
|
514
|
+
if (!narrowedVar) {
|
|
515
|
+
const narrowed = detectOptionalCheck(e.cond, ctx)
|
|
516
|
+
// Phase 3: && with optional check — (v !== undefined && ...) ? ... : ...
|
|
517
|
+
?? (e.cond.kind === "binop" && e.cond.op === "&&" ? detectOptionalCheck(e.cond.left, ctx) : null);
|
|
518
|
+
if (narrowed && narrowed.inThen) {
|
|
519
|
+
narrowedVar = narrowed.varName;
|
|
520
|
+
thenCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
|
|
521
|
+
if (narrowed.fieldExpr) {
|
|
522
|
+
narrowedExprResolved = narrowed.narrowedExpr ?? resolveExpr(narrowed.fieldExpr, ctx);
|
|
523
|
+
rawThen = substituteRawExpr(e.then, narrowed.fieldExpr, { kind: "var", name: narrowed.varName });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
let then_ = resolveExpr(rawThen, thenCtx);
|
|
326
528
|
let else_ = resolveExpr(e.else, ctx);
|
|
327
529
|
then_ = coerceStr(then_, else_.ty);
|
|
328
530
|
else_ = coerceStr(else_, then_.ty);
|
|
329
|
-
|
|
330
|
-
|
|
531
|
+
let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
|
|
532
|
+
// When one branch is undefined, result is optional
|
|
533
|
+
if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
|
|
534
|
+
ty = { kind: "optional", inner: else_.ty };
|
|
535
|
+
}
|
|
536
|
+
else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
|
|
537
|
+
ty = { kind: "optional", inner: then_.ty };
|
|
538
|
+
}
|
|
539
|
+
// When narrowedExpr is set AND a branch is void, the match produces Optional
|
|
540
|
+
const hasVoidBranch = then_.ty.kind === "void" || else_.ty.kind === "void";
|
|
541
|
+
if (narrowedExprResolved && hasVoidBranch && ty.kind !== "optional") {
|
|
542
|
+
ty = { kind: "optional", inner: ty };
|
|
543
|
+
}
|
|
544
|
+
return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
|
|
331
545
|
}
|
|
332
546
|
case "emptyCollection": {
|
|
333
547
|
const ty = parseTsType(e.tsType);
|
|
334
|
-
|
|
548
|
+
const elems = e.initElems ? e.initElems.map(el => resolveExpr(el, ctx)) : [];
|
|
549
|
+
return { kind: "arrayLiteral", elems, ty };
|
|
335
550
|
}
|
|
336
551
|
case "havoc":
|
|
337
552
|
return { kind: "havoc", ty: resolveTsType(e.tsType, ctx.overrides) };
|
|
@@ -363,6 +578,15 @@ function resolveBlock(stmts, ctx) {
|
|
|
363
578
|
const [typed, nextEnv] = resolveStmt(s, withEnv(ctx, env));
|
|
364
579
|
result.push(typed);
|
|
365
580
|
env = nextEnv;
|
|
581
|
+
// Flow narrowing: if (x === undefined) { return } narrows x for rest of block.
|
|
582
|
+
// Field chains are excluded — resolve can't substitute in statement lists;
|
|
583
|
+
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
584
|
+
if (s.kind === "if" && s.then.length > 0 && s.then[s.then.length - 1].kind === "return" && s.else.length === 0) {
|
|
585
|
+
const narrowed = detectOptionalCheck(s.cond, withEnv(ctx, env));
|
|
586
|
+
if (narrowed && !narrowed.inThen && !narrowed.fieldExpr) {
|
|
587
|
+
env = extend(env, narrowed.varName, narrowed.innerTy);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
366
590
|
}
|
|
367
591
|
return result;
|
|
368
592
|
}
|
|
@@ -379,8 +603,16 @@ function resolveStmt(s, ctx) {
|
|
|
379
603
|
const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
|
|
380
604
|
return [{ kind: "assign", target: s.target, value: coerceStr(resolveExpr(s.value, ctx), targetTy) }, ctx.env];
|
|
381
605
|
}
|
|
382
|
-
case "return":
|
|
383
|
-
|
|
606
|
+
case "return": {
|
|
607
|
+
let value = coerceStr(resolveExpr(s.value, ctx), ctx.returnTy);
|
|
608
|
+
// Wrap non-optional return value in Some when function returns optional
|
|
609
|
+
// Skip if already optional, void, or undefined (which maps to None)
|
|
610
|
+
const isUndef = value.kind === "var" && value.name === "undefined";
|
|
611
|
+
if (ctx.returnTy.kind === "optional" && value.ty.kind !== "optional" && !isUndef) {
|
|
612
|
+
value = wrapSome(value, ctx.returnTy);
|
|
613
|
+
}
|
|
614
|
+
return [{ kind: "return", value }, ctx.env];
|
|
615
|
+
}
|
|
384
616
|
case "break":
|
|
385
617
|
return [{ kind: "break" }, ctx.env];
|
|
386
618
|
case "continue":
|
|
@@ -388,10 +620,14 @@ function resolveStmt(s, ctx) {
|
|
|
388
620
|
case "expr":
|
|
389
621
|
return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
|
|
390
622
|
case "if": {
|
|
391
|
-
// Narrow optional<T> → T when checking !== undefined or undefined
|
|
623
|
+
// Narrow optional<T> → T when checking !== undefined or undefined !==.
|
|
624
|
+
// Also checks left side of && conditions: if (x !== undefined && ...) { ... }
|
|
625
|
+
// Field chains are excluded — resolve can't substitute in statement bodies;
|
|
626
|
+
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
392
627
|
let thenCtx = ctx, elseCtx = ctx;
|
|
393
|
-
const narrowed =
|
|
394
|
-
|
|
628
|
+
const narrowed = detectOptionalCheck(s.cond, ctx)
|
|
629
|
+
?? (s.cond.kind === "binop" && s.cond.op === "&&" ? detectOptionalCheck(s.cond.left, ctx) : null);
|
|
630
|
+
if (narrowed && !narrowed.fieldExpr) {
|
|
395
631
|
const env = extend(ctx.env, narrowed.varName, narrowed.innerTy);
|
|
396
632
|
if (narrowed.inThen)
|
|
397
633
|
thenCtx = withEnv(ctx, env);
|
|
@@ -483,7 +719,7 @@ function isSyntacticallyPure(stmts) {
|
|
|
483
719
|
case "while":
|
|
484
720
|
case "forof": return false;
|
|
485
721
|
case "let":
|
|
486
|
-
if (s.mutable)
|
|
722
|
+
if (s.mutable || s.init.kind === "havoc")
|
|
487
723
|
return false;
|
|
488
724
|
break;
|
|
489
725
|
case "if":
|
|
@@ -644,24 +880,28 @@ function containsReturn(stmts) {
|
|
|
644
880
|
return false;
|
|
645
881
|
}
|
|
646
882
|
// ── Resolve function / module ────────────────────────────────
|
|
647
|
-
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map()) {
|
|
648
|
-
if (hasReturnInLoop(fn.body)) {
|
|
649
|
-
throw new Error(`${fn.name}: return inside a loop is not supported.`);
|
|
650
|
-
}
|
|
883
|
+
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
|
|
651
884
|
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
652
885
|
const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
|
|
653
886
|
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
654
887
|
let env = null;
|
|
888
|
+
if (opts?.thisBinding)
|
|
889
|
+
env = extend(env, opts.thisBinding.name, opts.thisBinding.ty);
|
|
655
890
|
for (const p of params)
|
|
656
891
|
env = extend(env, p.name, p.ty);
|
|
657
892
|
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
|
|
658
893
|
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
659
894
|
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
895
|
+
// Apply type parameter constraints from //@ type T (==) annotations
|
|
896
|
+
const typeParams = fn.typeParams.map(tp => {
|
|
897
|
+
const constraint = overrides.get(tp);
|
|
898
|
+
return constraint ? `${tp}${constraint}` : tp;
|
|
899
|
+
});
|
|
660
900
|
return {
|
|
661
|
-
name: fn.name, params, returnTy,
|
|
901
|
+
name: fn.name, typeParams, params, returnTy,
|
|
662
902
|
requires: resolveSpecs(fn.requires, requiresCtx),
|
|
663
903
|
ensures: resolveSpecs(fn.ensures, ensuresCtx),
|
|
664
|
-
isPure: pureFns.has(fn.name),
|
|
904
|
+
isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
|
|
665
905
|
body: resolveBlock(fn.body, baseCtx),
|
|
666
906
|
};
|
|
667
907
|
}
|
|
@@ -669,31 +909,31 @@ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
|
|
|
669
909
|
const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
|
|
670
910
|
// Create a synthetic record type for 'this' so field access resolves
|
|
671
911
|
const thisType = { kind: "user", name: cls.name };
|
|
672
|
-
const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType })) };
|
|
912
|
+
const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType, type: parseTsType(f.tsType) })) };
|
|
673
913
|
const allTypeDecls = [...typeDecls, thisDecl];
|
|
674
|
-
const methods = cls.methods.map(fn => {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
679
|
-
let env = null;
|
|
680
|
-
env = extend(env, "this", thisType);
|
|
681
|
-
for (const p of params)
|
|
682
|
-
env = extend(env, p.name, p.ty);
|
|
683
|
-
const baseCtx = { env, typeDecls: allTypeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
|
|
684
|
-
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
685
|
-
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
686
|
-
return {
|
|
687
|
-
name: fn.name, params, returnTy,
|
|
688
|
-
requires: resolveSpecs(fn.requires, requiresCtx),
|
|
689
|
-
ensures: resolveSpecs(fn.ensures, ensuresCtx),
|
|
690
|
-
isPure: false, // class methods are never pure (they access this)
|
|
691
|
-
body: resolveBlock(fn.body, baseCtx),
|
|
692
|
-
};
|
|
693
|
-
});
|
|
914
|
+
const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, {
|
|
915
|
+
thisBinding: { name: "this", ty: thisType },
|
|
916
|
+
forcePure: false, // class methods are never pure (they access this)
|
|
917
|
+
}));
|
|
694
918
|
return { name: cls.name, fields, methods };
|
|
695
919
|
}
|
|
920
|
+
/** Pre-compute Ty on all TypeDeclInfo fields/variants/aliases.
|
|
921
|
+
* Called once per module so consumers can read field.type instead of re-parsing tsType. */
|
|
922
|
+
function precomputeFieldTypes(typeDecls) {
|
|
923
|
+
for (const d of typeDecls) {
|
|
924
|
+
if (d.fields)
|
|
925
|
+
for (const f of d.fields)
|
|
926
|
+
f.type = parseTsType(f.tsType);
|
|
927
|
+
if (d.variants)
|
|
928
|
+
for (const v of d.variants)
|
|
929
|
+
for (const f of v.fields)
|
|
930
|
+
f.type = parseTsType(f.tsType);
|
|
931
|
+
if (d.aliasOf && !d.aliasOfTy)
|
|
932
|
+
d.aliasOfTy = parseTsType(d.aliasOf);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
696
935
|
export function resolveModule(raw) {
|
|
936
|
+
precomputeFieldTypes(raw.typeDecls);
|
|
697
937
|
const pureFns = computePureFns(raw.functions);
|
|
698
938
|
// Pre-compute function parameter types for optional coercion
|
|
699
939
|
const fnParams = new Map();
|
package/tools/dist/specparser.js
CHANGED
|
@@ -129,6 +129,11 @@ class Parser {
|
|
|
129
129
|
parseCmp() {
|
|
130
130
|
const left = this.parseAdd();
|
|
131
131
|
const t = this.peek();
|
|
132
|
+
// 'in' as infix membership operator (set/seq/map): x in S
|
|
133
|
+
if (t?.type === "ident" && t.value === "in") {
|
|
134
|
+
this.advance();
|
|
135
|
+
return { kind: "binop", op: "in", left, right: this.parseAdd() };
|
|
136
|
+
}
|
|
132
137
|
if (t?.type === "op" && ["===", "!==", "==", "!=", ">=", "<=", ">", "<"].includes(t.value)) {
|
|
133
138
|
this.advance();
|
|
134
139
|
// Normalize == to ===, != to !== so downstream sees one spelling
|
|
@@ -252,8 +257,6 @@ class Parser {
|
|
|
252
257
|
let varType = "int";
|
|
253
258
|
if (this.match("punc", ":")) {
|
|
254
259
|
const ty = this.expect("ident").value;
|
|
255
|
-
if (ty !== "nat" && ty !== "int")
|
|
256
|
-
throw new Error(`Unknown type '${ty}'`);
|
|
257
260
|
varType = ty;
|
|
258
261
|
}
|
|
259
262
|
this.expect("punc", ",");
|