lemmascript 0.3.3 → 0.4.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 +4 -2
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +31 -14
- package/tools/dist/dafny-emit.js +17 -5
- package/tools/dist/extract.js +32 -20
- package/tools/dist/lean-emit.js +53 -3
- package/tools/dist/lsc.js +13 -3
- package/tools/dist/narrow.js +737 -0
- package/tools/dist/peephole.js +448 -0
- package/tools/dist/resolve.js +313 -220
- package/tools/dist/specparser.js +12 -2
- package/tools/dist/transform.js +217 -419
package/tools/dist/resolve.js
CHANGED
|
@@ -6,72 +6,6 @@
|
|
|
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
|
-
}
|
|
75
9
|
function lookup(env, name) {
|
|
76
10
|
if (!env)
|
|
77
11
|
return undefined;
|
|
@@ -80,6 +14,21 @@ function lookup(env, name) {
|
|
|
80
14
|
function extend(env, name, ty) {
|
|
81
15
|
return { name, ty, parent: env };
|
|
82
16
|
}
|
|
17
|
+
function asRawAccessPath(e) {
|
|
18
|
+
if (e.kind === "var")
|
|
19
|
+
return { rootVar: e.name, fields: [] };
|
|
20
|
+
if (e.kind === "field") {
|
|
21
|
+
const inner = asRawAccessPath(e.obj);
|
|
22
|
+
if (!inner)
|
|
23
|
+
return null;
|
|
24
|
+
return { rootVar: inner.rootVar, fields: [...inner.fields, e.field] };
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function accessPathsEqual(a, b) {
|
|
29
|
+
return a.rootVar === b.rootVar && a.fields.length === b.fields.length &&
|
|
30
|
+
a.fields.every((f, i) => f === b.fields[i]);
|
|
31
|
+
}
|
|
83
32
|
function withEnv(ctx, env) {
|
|
84
33
|
return { ...ctx, env };
|
|
85
34
|
}
|
|
@@ -106,17 +55,25 @@ function wrapSome(value, optionalTy) {
|
|
|
106
55
|
args: [value], ty: optionalTy, callKind: "pure",
|
|
107
56
|
};
|
|
108
57
|
}
|
|
109
|
-
/** Detect `v !== undefined`
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
58
|
+
/** Detect optional checks: `v !== undefined` (positive narrows then-branch),
|
|
59
|
+
* `v === undefined` (negative narrows else-branch), or `!v` (equivalent to
|
|
60
|
+
* `=== undefined`).
|
|
61
|
+
* Returns:
|
|
62
|
+
* - simple var: `varName` set, `fieldExpr` unset
|
|
63
|
+
* - complex (field chain or call): `fieldExpr` set, `varName` empty
|
|
64
|
+
* - inThen: true for `!==` (truthy), false for `===` and `!v` (falsy).
|
|
65
|
+
* Does NOT recurse into `&&`. */
|
|
117
66
|
function detectOptionalCheck(cond, ctx) {
|
|
118
|
-
|
|
119
|
-
|
|
67
|
+
// `!v` where v is optional — same shape as `v === undefined` (inThen: false).
|
|
68
|
+
if (cond.kind === "unop" && cond.op === "!") {
|
|
69
|
+
const inner = classifyOptExpr(cond.expr, ctx);
|
|
70
|
+
return inner ? { ...inner, inThen: false } : null;
|
|
71
|
+
}
|
|
72
|
+
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
|
|
73
|
+
// Bare optional truthiness: `if (v)` where v: T | undefined — same as `v !== undefined`.
|
|
74
|
+
const inner = classifyOptExpr(cond, ctx);
|
|
75
|
+
return inner ? { ...inner, inThen: true } : null;
|
|
76
|
+
}
|
|
120
77
|
// Identify the expression being checked against undefined
|
|
121
78
|
let optExpr = null;
|
|
122
79
|
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
@@ -125,23 +82,22 @@ function detectOptionalCheck(cond, ctx) {
|
|
|
125
82
|
optExpr = cond.right;
|
|
126
83
|
if (!optExpr)
|
|
127
84
|
return null;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
85
|
+
const inner = classifyOptExpr(optExpr, ctx);
|
|
86
|
+
return inner ? { ...inner, inThen: cond.op === "!==" } : null;
|
|
87
|
+
}
|
|
88
|
+
/** Classify an expression as a simple var or field-chain optional, returning
|
|
89
|
+
* the shape needed by detectOptionalCheck (sans inThen). */
|
|
90
|
+
function classifyOptExpr(e, ctx) {
|
|
91
|
+
if (e.kind === "var") {
|
|
92
|
+
const ty = lookup(ctx.env, e.name);
|
|
131
93
|
if (!ty || ty.kind !== "optional")
|
|
132
94
|
return null;
|
|
133
|
-
return { varName:
|
|
134
|
-
}
|
|
135
|
-
// Field access chain or arbitrary expression — resolve to check type, needs substitution
|
|
136
|
-
const resolved = resolveExpr(optExpr, ctx);
|
|
137
|
-
if (resolved.ty.kind === "optional") {
|
|
138
|
-
const synVar = optExpr.kind === "field" ? `_narr${_synVarCounter++}` : `_opt${_synVarCounter++}`;
|
|
139
|
-
return {
|
|
140
|
-
varName: synVar, innerTy: resolved.ty.inner, inThen: cond.op === "!==",
|
|
141
|
-
fieldExpr: optExpr, narrowedExpr: resolved,
|
|
142
|
-
};
|
|
95
|
+
return { varName: e.name, innerTy: ty.inner };
|
|
143
96
|
}
|
|
144
|
-
|
|
97
|
+
const resolved = resolveExpr(e, ctx);
|
|
98
|
+
if (resolved.ty.kind !== "optional")
|
|
99
|
+
return null;
|
|
100
|
+
return { varName: "", innerTy: resolved.ty.inner, fieldExpr: e };
|
|
145
101
|
}
|
|
146
102
|
/** Collect all optional narrowings from an early-return condition.
|
|
147
103
|
* Handles single checks (x === undefined) and compound || chains
|
|
@@ -156,6 +112,82 @@ function collectEarlyReturnNarrowings(cond, ctx) {
|
|
|
156
112
|
}
|
|
157
113
|
return [];
|
|
158
114
|
}
|
|
115
|
+
/** TExpr → AccessPath. Counterpart to `asRawAccessPath` for resolved trees.
|
|
116
|
+
* Used by `extractInAtoms` when pulling atoms out of typed spec expressions. */
|
|
117
|
+
function asTExprAccessPath(e) {
|
|
118
|
+
if (e.kind === "var")
|
|
119
|
+
return { rootVar: e.name, fields: [] };
|
|
120
|
+
if (e.kind === "field") {
|
|
121
|
+
const inner = asTExprAccessPath(e.obj);
|
|
122
|
+
if (!inner)
|
|
123
|
+
return null;
|
|
124
|
+
return { rootVar: inner.rootVar, fields: [...inner.fields, e.field] };
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/** Walk `e` collecting top-level `k in m` atoms where both sides are pure
|
|
129
|
+
* access paths and the right side is map-typed. Descends through `&&` only.
|
|
130
|
+
* Does NOT descend into `==>`, `||`, negation, `forall`, or `exists` — in
|
|
131
|
+
* those positions an atom is only conditionally known (or a premise, not a
|
|
132
|
+
* conclusion), so treating it as always-true in the enclosing scope would
|
|
133
|
+
* be unsound. */
|
|
134
|
+
function extractInAtoms(e) {
|
|
135
|
+
if (e.kind === "binop" && e.op === "in" && e.right.ty.kind === "map") {
|
|
136
|
+
const obj = asTExprAccessPath(e.right);
|
|
137
|
+
const idx = asTExprAccessPath(e.left);
|
|
138
|
+
if (obj && idx)
|
|
139
|
+
return [{ obj, idx }];
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
if (e.kind === "binop" && e.op === "&&") {
|
|
143
|
+
return [...extractInAtoms(e.left), ...extractInAtoms(e.right)];
|
|
144
|
+
}
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
/** Extract `k in m` atoms that hold when `e` is *false*. Currently only
|
|
148
|
+
* strips an outer `!` and hands the inner to `extractInAtoms`; that covers
|
|
149
|
+
* `if (!(k in m)) ...` for the else-branch and early-return patterns.
|
|
150
|
+
* De Morgan over `||` / nested `!(a && b)` not handled yet. */
|
|
151
|
+
function extractInAtomsNegated(e) {
|
|
152
|
+
if (e.kind === "unop" && e.op === "!")
|
|
153
|
+
return extractInAtoms(e.expr);
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
/** Extend a Ctx with `k in m` atoms. Deduplicates against existing atoms. */
|
|
157
|
+
function withInAtoms(ctx, atoms) {
|
|
158
|
+
if (atoms.length === 0)
|
|
159
|
+
return ctx;
|
|
160
|
+
const existing = ctx.narrowedIndices;
|
|
161
|
+
const added = [];
|
|
162
|
+
for (const a of atoms) {
|
|
163
|
+
if (!existing.some(e => accessPathsEqual(e.obj, a.obj) && accessPathsEqual(e.idx, a.idx))) {
|
|
164
|
+
added.push(a);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (added.length === 0)
|
|
168
|
+
return ctx;
|
|
169
|
+
return { ...ctx, narrowedIndices: [...existing, ...added] };
|
|
170
|
+
}
|
|
171
|
+
/** Walk an `&&` chain of `e !== undefined` checks, returning a Ctx with all
|
|
172
|
+
* narrowings applied. Earlier checks are in scope for later checks (so the
|
|
173
|
+
* right side of `&&` sees the left side's narrowings). */
|
|
174
|
+
function collectAndChainNarrowings(cond, ctx) {
|
|
175
|
+
if (cond.kind === "binop" && cond.op === "&&") {
|
|
176
|
+
const leftCtx = collectAndChainNarrowings(cond.left, ctx);
|
|
177
|
+
return collectAndChainNarrowings(cond.right, leftCtx);
|
|
178
|
+
}
|
|
179
|
+
const n = detectOptionalCheck(cond, ctx);
|
|
180
|
+
if (!n || !n.inThen)
|
|
181
|
+
return ctx;
|
|
182
|
+
if (!n.fieldExpr) {
|
|
183
|
+
return withEnv(ctx, extend(ctx.env, n.varName, n.innerTy));
|
|
184
|
+
}
|
|
185
|
+
const path = asRawAccessPath(n.fieldExpr);
|
|
186
|
+
if (path) {
|
|
187
|
+
return { ...ctx, narrowedPaths: [...ctx.narrowedPaths, { path, narrowedTy: n.innerTy }] };
|
|
188
|
+
}
|
|
189
|
+
return ctx;
|
|
190
|
+
}
|
|
159
191
|
/** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
|
|
160
192
|
function isRefMutableInTS(ty) {
|
|
161
193
|
return ty.kind === "array" || ty.kind === "map" || ty.kind === "set";
|
|
@@ -332,6 +364,34 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
332
364
|
}
|
|
333
365
|
return { kind: "unknown" };
|
|
334
366
|
}
|
|
367
|
+
/** Look up the type of `field` on `objTy`. Returns `unknown` if not found. */
|
|
368
|
+
function lookupFieldTy(objTy, field, ctx) {
|
|
369
|
+
if (field === "length" && (objTy.kind === "array" || objTy.kind === "string")) {
|
|
370
|
+
return { ty: { kind: "nat" }, isDiscriminant: false };
|
|
371
|
+
}
|
|
372
|
+
if (field === "size" && (objTy.kind === "map" || objTy.kind === "set")) {
|
|
373
|
+
return { ty: { kind: "nat" }, isDiscriminant: false };
|
|
374
|
+
}
|
|
375
|
+
if (objTy.kind === "user") {
|
|
376
|
+
const baseTyName = objTy.name.includes("<") ? objTy.name.slice(0, objTy.name.indexOf("<")) : objTy.name;
|
|
377
|
+
const isDiscriminant = getDiscriminant(ctx, baseTyName) === field;
|
|
378
|
+
const decl = findDecl(ctx, baseTyName);
|
|
379
|
+
if (decl?.kind === "record") {
|
|
380
|
+
const f = decl.fields?.find(f => f.name === field);
|
|
381
|
+
if (f)
|
|
382
|
+
return { ty: f.type, isDiscriminant };
|
|
383
|
+
}
|
|
384
|
+
if (decl?.kind === "discriminated-union" && decl.variants) {
|
|
385
|
+
for (const variant of decl.variants) {
|
|
386
|
+
const f = variant.fields.find(f => f.name === field);
|
|
387
|
+
if (f)
|
|
388
|
+
return { ty: f.type, isDiscriminant };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { ty: { kind: "unknown" }, isDiscriminant };
|
|
392
|
+
}
|
|
393
|
+
return { ty: { kind: "unknown" }, isDiscriminant: false };
|
|
394
|
+
}
|
|
335
395
|
// ── Resolve expressions ──────────────────────────────────────
|
|
336
396
|
function resolveExpr(e, ctx) {
|
|
337
397
|
switch (e.kind) {
|
|
@@ -359,16 +419,12 @@ function resolveExpr(e, ctx) {
|
|
|
359
419
|
}
|
|
360
420
|
case "binop": {
|
|
361
421
|
let left = resolveExpr(e.left, ctx);
|
|
362
|
-
// && narrowing:
|
|
363
|
-
//
|
|
364
|
-
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
422
|
+
// && and ==> narrowing: left-side optional checks narrow the right side.
|
|
423
|
+
// (For ==>, the premise is assumed in the conclusion — same principle.)
|
|
365
424
|
let rightCtx = ctx;
|
|
366
425
|
let rawRight = e.right;
|
|
367
|
-
if (e.op === "&&") {
|
|
368
|
-
|
|
369
|
-
if (narrowed && narrowed.inThen && !narrowed.fieldExpr) {
|
|
370
|
-
rightCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
|
|
371
|
-
}
|
|
426
|
+
if (e.op === "&&" || e.op === "==>") {
|
|
427
|
+
rightCtx = collectAndChainNarrowings(e.left, ctx);
|
|
372
428
|
}
|
|
373
429
|
let right = resolveExpr(rawRight, rightCtx);
|
|
374
430
|
if (e.op === "===" || e.op === "!==") {
|
|
@@ -424,50 +480,102 @@ function resolveExpr(e, ctx) {
|
|
|
424
480
|
case "index": {
|
|
425
481
|
const obj = resolveExpr(e.obj, ctx);
|
|
426
482
|
const idx = resolveExpr(e.idx, ctx);
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
483
|
+
// Map bracket access: default to Option<V>. But if the enclosing scope has a
|
|
484
|
+
// known `k in m` atom matching (obj, idx) — from requires, assert, an enclosing
|
|
485
|
+
// `if (k in m)`, or a loop invariant — narrow to V. Parallels how `narrowedPaths`
|
|
486
|
+
// narrows `obj.field.field` under optional-undefined checks.
|
|
487
|
+
let idxTy;
|
|
488
|
+
if (obj.ty.kind === "array") {
|
|
489
|
+
idxTy = obj.ty.elem;
|
|
490
|
+
}
|
|
491
|
+
else if (obj.ty.kind === "map") {
|
|
492
|
+
const objPath = asTExprAccessPath(obj);
|
|
493
|
+
const idxPath = asTExprAccessPath(idx);
|
|
494
|
+
const narrowed = objPath && idxPath && ctx.narrowedIndices.some(n => accessPathsEqual(n.obj, objPath) && accessPathsEqual(n.idx, idxPath));
|
|
495
|
+
idxTy = narrowed ? obj.ty.value : { kind: "optional", inner: obj.ty.value };
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
idxTy = { kind: "unknown" };
|
|
499
|
+
}
|
|
430
500
|
return { kind: "index", obj, idx, ty: idxTy };
|
|
431
501
|
}
|
|
432
502
|
case "field": {
|
|
433
503
|
const obj = resolveExpr(e.obj, ctx);
|
|
434
504
|
let isDiscriminant = false;
|
|
435
505
|
let ty = { kind: "unknown" };
|
|
436
|
-
// Check narrowed
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
506
|
+
// Check narrowed path context (from conditional optional checks).
|
|
507
|
+
// Applies when the current field-access forms a pure access path AND
|
|
508
|
+
// that path is in the narrowedPaths list.
|
|
509
|
+
if (ctx.narrowedPaths.length > 0) {
|
|
510
|
+
const myPath = asRawAccessPath(e);
|
|
511
|
+
if (myPath) {
|
|
512
|
+
const np = ctx.narrowedPaths.find(n => accessPathsEqual(n.path, myPath));
|
|
513
|
+
if (np)
|
|
514
|
+
ty = np.narrowedTy;
|
|
515
|
+
}
|
|
444
516
|
}
|
|
445
|
-
|
|
446
|
-
|
|
517
|
+
if (ty.kind === "unknown") {
|
|
518
|
+
const lookup = lookupFieldTy(obj.ty, e.field, ctx);
|
|
519
|
+
ty = lookup.ty;
|
|
520
|
+
isDiscriminant = lookup.isDiscriminant;
|
|
447
521
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
522
|
+
return { kind: "field", obj, field: e.field, ty, isDiscriminant };
|
|
523
|
+
}
|
|
524
|
+
case "nullish": {
|
|
525
|
+
// left ?? right — result type is left's inner (when left is optional)
|
|
526
|
+
// or just left's type, unified with right's type.
|
|
527
|
+
const left = resolveExpr(e.left, ctx);
|
|
528
|
+
const right = resolveExpr(e.right, ctx);
|
|
529
|
+
const ty = left.ty.kind === "optional" ? left.ty.inner : left.ty;
|
|
530
|
+
return { kind: "nullish", left, right, ty };
|
|
531
|
+
}
|
|
532
|
+
case "optChain": {
|
|
533
|
+
// obj?.<chain> — obj has type Option<T>; we walk the chain stepping
|
|
534
|
+
// through types from T. The final result is Option<finalStepTy>
|
|
535
|
+
// (collapsed: if finalStepTy is already optional, we don't double-wrap).
|
|
536
|
+
// Narrow rewrites this to a someMatch with the chain applied to the binder.
|
|
537
|
+
const obj = resolveExpr(e.obj, ctx);
|
|
538
|
+
let stepInTy = obj.ty.kind === "optional" ? obj.ty.inner : obj.ty;
|
|
539
|
+
const chain = [];
|
|
540
|
+
for (const step of e.chain) {
|
|
541
|
+
if (step.kind === "field") {
|
|
542
|
+
const fieldTy = lookupFieldTy(stepInTy, step.name, ctx).ty;
|
|
543
|
+
chain.push({ kind: "field", name: step.name, ty: fieldTy });
|
|
544
|
+
stepInTy = fieldTy;
|
|
545
|
+
}
|
|
546
|
+
else if (step.kind === "index") {
|
|
547
|
+
const idx = resolveExpr(step.idx, ctx);
|
|
548
|
+
const idxTy = stepInTy.kind === "array" ? stepInTy.elem
|
|
549
|
+
: stepInTy.kind === "map" ? { kind: "optional", inner: stepInTy.value }
|
|
550
|
+
: { kind: "unknown" };
|
|
551
|
+
chain.push({ kind: "index", idx, ty: idxTy });
|
|
552
|
+
stepInTy = idxTy;
|
|
458
553
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
554
|
+
else {
|
|
555
|
+
// call: prev step yielded a callable (typically a method via field).
|
|
556
|
+
// Build a fake fn TExpr from prev steps to reuse inferMethodReturnTy.
|
|
557
|
+
const args = step.args.map(a => resolveExpr(a, ctx));
|
|
558
|
+
const lastField = chain.length > 0 && chain[chain.length - 1].kind === "field"
|
|
559
|
+
? chain[chain.length - 1] : null;
|
|
560
|
+
let callTy = { kind: "unknown" };
|
|
561
|
+
let callKind = "unknown";
|
|
562
|
+
if (lastField) {
|
|
563
|
+
// Build a synthetic field TExpr with the prior step's input type as obj
|
|
564
|
+
// so inferMethodReturnTy can dispatch on the receiver type.
|
|
565
|
+
const priorInTy = chain.length >= 2 ? chain[chain.length - 2].ty
|
|
566
|
+
: (obj.ty.kind === "optional" ? obj.ty.inner : obj.ty);
|
|
567
|
+
const fakeObj = { kind: "var", name: "_chain_recv", ty: priorInTy };
|
|
568
|
+
const fakeFn = { kind: "field", obj: fakeObj, field: lastField.name, ty: lastField.ty };
|
|
569
|
+
callTy = inferMethodReturnTy(fakeFn, args, ctx);
|
|
570
|
+
callKind = "method";
|
|
467
571
|
}
|
|
572
|
+
chain.push({ kind: "call", args, ty: callTy, callKind });
|
|
573
|
+
stepInTy = callTy;
|
|
468
574
|
}
|
|
469
575
|
}
|
|
470
|
-
|
|
576
|
+
const finalTy = stepInTy;
|
|
577
|
+
const ty = finalTy.kind === "optional" ? finalTy : { kind: "optional", inner: finalTy };
|
|
578
|
+
return { kind: "optChain", obj, chain, ty };
|
|
471
579
|
}
|
|
472
580
|
case "record": {
|
|
473
581
|
const spread = e.spread ? resolveExpr(e.spread, ctx) : null;
|
|
@@ -539,87 +647,43 @@ function resolveExpr(e, ctx) {
|
|
|
539
647
|
}
|
|
540
648
|
case "conditional": {
|
|
541
649
|
const cond = resolveExpr(e.cond, ctx);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (e.cond.kind === "var") {
|
|
549
|
-
narrowedVar = e.cond.name;
|
|
550
|
-
thenCtx = withEnv(ctx, extend(ctx.env, e.cond.name, innerTy));
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
narrowedVar = `_opt${_synVarCounter++}`;
|
|
554
|
-
rawThen = substituteRawExpr(e.then, e.cond, { kind: "var", name: narrowedVar });
|
|
555
|
-
thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
// Phase 2/3: Explicit check — v !== undefined, or && with optional check.
|
|
559
|
-
// Resolve only narrows the type environment; transform handles all structural
|
|
560
|
-
// narrowing (match generation, variable binding, && splitting).
|
|
561
|
-
let narrowedExprResolved;
|
|
650
|
+
// Type narrowing for the then/else branches. Following TS, we narrow
|
|
651
|
+
// simple vars and any pure access path (`a.b.c.d`) — but not expressions
|
|
652
|
+
// with method calls or index ops (bind-first required).
|
|
653
|
+
// For &&-chains, all positive checks narrow the then-branch; earlier
|
|
654
|
+
// checks are in scope when resolving later ones.
|
|
655
|
+
let thenCtx = collectAndChainNarrowings(e.cond, ctx);
|
|
562
656
|
let elseCtx = ctx;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
narrowedFields: [...thenCtx.narrowedFields, {
|
|
576
|
-
objName: narrowed.fieldExpr.obj.name,
|
|
577
|
-
fieldName: narrowed.fieldExpr.field,
|
|
578
|
-
narrowedTy: narrowed.innerTy,
|
|
579
|
-
}],
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
else {
|
|
583
|
-
// Complex expression (call result, deep chain, etc.): transform can't detect these,
|
|
584
|
-
// so keep old behavior — substitute + narrowedVar + narrowedExpr
|
|
585
|
-
narrowedVar = narrowed.varName;
|
|
586
|
-
narrowedExprResolved = narrowed.narrowedExpr ?? resolveExpr(narrowed.fieldExpr, ctx);
|
|
587
|
-
rawThen = substituteRawExpr(e.then, narrowed.fieldExpr, { kind: "var", name: narrowed.varName });
|
|
588
|
-
thenCtx = withEnv(thenCtx, extend(thenCtx.env, narrowed.varName, narrowed.innerTy));
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
else if (narrowed && !narrowed.inThen && !narrowed.fieldExpr) {
|
|
592
|
-
// v === undefined: narrow v in the else branch
|
|
593
|
-
elseCtx = withEnv(elseCtx, extend(elseCtx.env, narrowed.varName, narrowed.innerTy));
|
|
594
|
-
}
|
|
595
|
-
// Compound || with === undefined: narrow all checked vars in else branch
|
|
596
|
-
// e.g. if (a === undefined || b === undefined) then X else Y → narrow a,b in Y
|
|
597
|
-
// TODO: resolve-time narrowing works but transform doesn't emit match unwrap
|
|
598
|
-
// for || conditions yet — decompose || into nested matches in transform.
|
|
599
|
-
// Workaround: split || into separate if guards in user code.
|
|
600
|
-
if (!narrowed && e.cond.kind === "binop" && e.cond.op === "||") {
|
|
601
|
-
for (const n of collectEarlyReturnNarrowings(e.cond, ctx)) {
|
|
602
|
-
elseCtx = withEnv(elseCtx, extend(elseCtx.env, n.varName, n.innerTy));
|
|
603
|
-
}
|
|
657
|
+
// Truthiness — cond itself is optional (`opt ? a : b`), only for simple vars.
|
|
658
|
+
if (cond.ty.kind === "optional" && e.cond.kind === "var") {
|
|
659
|
+
thenCtx = withEnv(thenCtx, extend(thenCtx.env, e.cond.name, cond.ty.inner));
|
|
660
|
+
}
|
|
661
|
+
// Single === undefined check narrows the else-branch.
|
|
662
|
+
const single = detectOptionalCheck(e.cond, ctx);
|
|
663
|
+
if (single && !single.inThen && !single.fieldExpr) {
|
|
664
|
+
elseCtx = withEnv(elseCtx, extend(elseCtx.env, single.varName, single.innerTy));
|
|
665
|
+
}
|
|
666
|
+
if (!single && e.cond.kind === "binop" && e.cond.op === "||") {
|
|
667
|
+
for (const n of collectEarlyReturnNarrowings(e.cond, ctx)) {
|
|
668
|
+
elseCtx = withEnv(elseCtx, extend(elseCtx.env, n.varName, n.innerTy));
|
|
604
669
|
}
|
|
605
670
|
}
|
|
606
|
-
|
|
671
|
+
// Map-index narrowing: `k in m` in the cond narrows the then-branch; `!(k in m)`
|
|
672
|
+
// narrows the else-branch. Parallel to the if-statement hook in resolveStmt.
|
|
673
|
+
thenCtx = withInAtoms(thenCtx, extractInAtoms(cond));
|
|
674
|
+
elseCtx = withInAtoms(elseCtx, extractInAtomsNegated(cond));
|
|
675
|
+
let then_ = resolveExpr(e.then, thenCtx);
|
|
607
676
|
let else_ = resolveExpr(e.else, elseCtx);
|
|
608
677
|
then_ = coerceStr(then_, else_.ty);
|
|
609
678
|
else_ = coerceStr(else_, then_.ty);
|
|
610
679
|
let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
|
|
611
|
-
// When one branch is undefined, result is optional
|
|
612
680
|
if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
|
|
613
681
|
ty = { kind: "optional", inner: else_.ty };
|
|
614
682
|
}
|
|
615
683
|
else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
|
|
616
684
|
ty = { kind: "optional", inner: then_.ty };
|
|
617
685
|
}
|
|
618
|
-
|
|
619
|
-
if (narrowedExprResolved && (then_.ty.kind === "void" || else_.ty.kind === "void") && ty.kind !== "optional") {
|
|
620
|
-
ty = { kind: "optional", inner: ty };
|
|
621
|
-
}
|
|
622
|
-
return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
|
|
686
|
+
return { kind: "conditional", cond, then: then_, else: else_, ty };
|
|
623
687
|
}
|
|
624
688
|
case "emptyCollection": {
|
|
625
689
|
const ty = parseTsType(e.tsType);
|
|
@@ -652,8 +716,10 @@ function splitConj(e) {
|
|
|
652
716
|
function resolveBlock(stmts, ctx) {
|
|
653
717
|
const result = [];
|
|
654
718
|
let env = ctx.env;
|
|
719
|
+
let narrowedIndices = ctx.narrowedIndices;
|
|
655
720
|
for (const s of stmts) {
|
|
656
|
-
const
|
|
721
|
+
const currentCtx = { ...ctx, env, narrowedIndices };
|
|
722
|
+
const [typed, nextEnv] = resolveStmt(s, currentCtx);
|
|
657
723
|
result.push(typed);
|
|
658
724
|
env = nextEnv;
|
|
659
725
|
// Flow narrowing: if (x === undefined) { return } narrows x for rest of block.
|
|
@@ -665,6 +731,20 @@ function resolveBlock(stmts, ctx) {
|
|
|
665
731
|
for (const n of narrowings) {
|
|
666
732
|
env = extend(env, n.varName, n.innerTy);
|
|
667
733
|
}
|
|
734
|
+
// Map-index narrowing: `if (!(k in m)) return;` means `k in m` holds in rest.
|
|
735
|
+
if (typed.kind === "if") {
|
|
736
|
+
const addedAtoms = extractInAtomsNegated(typed.cond);
|
|
737
|
+
if (addedAtoms.length > 0) {
|
|
738
|
+
narrowedIndices = withInAtoms({ ...ctx, narrowedIndices }, addedAtoms).narrowedIndices;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// Assert narrowing: `//@ assert k in m` adds atoms for the rest of the block.
|
|
743
|
+
if (typed.kind === "assert") {
|
|
744
|
+
const addedAtoms = extractInAtoms(typed.expr);
|
|
745
|
+
if (addedAtoms.length > 0) {
|
|
746
|
+
narrowedIndices = withInAtoms({ ...ctx, narrowedIndices }, addedAtoms).narrowedIndices;
|
|
747
|
+
}
|
|
668
748
|
}
|
|
669
749
|
}
|
|
670
750
|
return result;
|
|
@@ -705,30 +785,36 @@ function resolveStmt(s, ctx) {
|
|
|
705
785
|
return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
|
|
706
786
|
case "if": {
|
|
707
787
|
// Narrow optional<T> → T when checking !== undefined or undefined !==.
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
let thenCtx =
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
if (
|
|
715
|
-
|
|
716
|
-
if (narrowed.inThen)
|
|
717
|
-
thenCtx = withEnv(ctx, env);
|
|
718
|
-
else
|
|
719
|
-
elseCtx = withEnv(ctx, env);
|
|
788
|
+
// For &&-chains, all positive optional checks narrow the then-branch;
|
|
789
|
+
// earlier checks are in scope when resolving later ones.
|
|
790
|
+
// Single-check === undefined narrows the else-branch.
|
|
791
|
+
let thenCtx = collectAndChainNarrowings(s.cond, ctx);
|
|
792
|
+
let elseCtx = ctx;
|
|
793
|
+
const single = detectOptionalCheck(s.cond, ctx);
|
|
794
|
+
if (single && !single.inThen && !single.fieldExpr) {
|
|
795
|
+
elseCtx = withEnv(ctx, extend(ctx.env, single.varName, single.innerTy));
|
|
720
796
|
}
|
|
721
|
-
|
|
797
|
+
// Narrow map index access across `k in m` / `!(k in m)` in the cond:
|
|
798
|
+
// positive atoms (from `k in m` or &&-chains containing it) → then-branch;
|
|
799
|
+
// negated atoms (from `!(k in m)`) → else-branch.
|
|
800
|
+
const resolvedCond = resolveExpr(s.cond, ctx);
|
|
801
|
+
thenCtx = withInAtoms(thenCtx, extractInAtoms(resolvedCond));
|
|
802
|
+
elseCtx = withInAtoms(elseCtx, extractInAtomsNegated(resolvedCond));
|
|
803
|
+
return [{ kind: "if", cond: resolvedCond, then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
|
|
722
804
|
}
|
|
723
805
|
case "while": {
|
|
724
806
|
const whileSpecCtx = { ...ctx, inSpec: true };
|
|
807
|
+
const resolvedInvariants = resolveSpecs(s.invariants, whileSpecCtx);
|
|
808
|
+
// Invariants hold at the top of the body, so any `k in m` atoms among them
|
|
809
|
+
// narrow map index access in the body.
|
|
810
|
+
const bodyCtx = withInAtoms(ctx, resolvedInvariants.flatMap(extractInAtoms));
|
|
725
811
|
return [{
|
|
726
812
|
kind: "while",
|
|
727
813
|
cond: resolveExpr(s.cond, ctx),
|
|
728
|
-
invariants:
|
|
814
|
+
invariants: resolvedInvariants,
|
|
729
815
|
decreases: s.decreases ? resolveSpec(s.decreases, whileSpecCtx) : null,
|
|
730
816
|
doneWith: s.doneWith ? resolveSpec(s.doneWith, whileSpecCtx) : null,
|
|
731
|
-
body: resolveBlock(s.body,
|
|
817
|
+
body: resolveBlock(s.body, bodyCtx),
|
|
732
818
|
}, ctx.env];
|
|
733
819
|
}
|
|
734
820
|
case "forof": {
|
|
@@ -975,7 +1061,7 @@ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), fnReturns
|
|
|
975
1061
|
env = extend(env, opts.thisBinding.name, opts.thisBinding.ty);
|
|
976
1062
|
for (const p of params)
|
|
977
1063
|
env = extend(env, p.name, p.ty);
|
|
978
|
-
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, fnReturns, inSpec: false, inLambda: false,
|
|
1064
|
+
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, fnReturns, inSpec: false, inLambda: false, narrowedPaths: [], narrowedIndices: [] };
|
|
979
1065
|
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
980
1066
|
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
981
1067
|
// Apply type parameter constraints from //@ type T (==) annotations
|
|
@@ -983,14 +1069,22 @@ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), fnReturns
|
|
|
983
1069
|
const constraint = overrides.get(tp);
|
|
984
1070
|
return constraint ? `${tp}${constraint}` : tp;
|
|
985
1071
|
});
|
|
1072
|
+
// Resolve requires first so we can extract any `k in m` atoms and seed them
|
|
1073
|
+
// into the body context and the ensures context — they hold for the whole
|
|
1074
|
+
// body (pure fns) and for post-state references in the ensures (map params
|
|
1075
|
+
// aren't mutated through their binding in the Dafny translation).
|
|
1076
|
+
const resolvedRequires = resolveSpecs(fn.requires, requiresCtx);
|
|
1077
|
+
const requiresAtoms = resolvedRequires.flatMap(extractInAtoms);
|
|
1078
|
+
const bodyCtx = withInAtoms(baseCtx, requiresAtoms);
|
|
1079
|
+
const ensuresCtxNarrowed = withInAtoms(ensuresCtx, requiresAtoms);
|
|
986
1080
|
return {
|
|
987
1081
|
name: fn.name, typeParams, params, returnTy,
|
|
988
|
-
requires:
|
|
989
|
-
ensures: resolveSpecs(fn.ensures,
|
|
1082
|
+
requires: resolvedRequires,
|
|
1083
|
+
ensures: resolveSpecs(fn.ensures, ensuresCtxNarrowed),
|
|
990
1084
|
decreases: fn.decreases ? resolveSpec(fn.decreases, requiresCtx) : null,
|
|
991
1085
|
isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
|
|
992
1086
|
forcePure: fn.pure,
|
|
993
|
-
body: resolveBlock(fn.body,
|
|
1087
|
+
body: resolveBlock(fn.body, bodyCtx),
|
|
994
1088
|
};
|
|
995
1089
|
}
|
|
996
1090
|
function resolveClass(cls, typeDecls, pureFns, fnParams = new Map(), fnReturns = new Map()) {
|
|
@@ -1021,7 +1115,6 @@ function precomputeFieldTypes(typeDecls) {
|
|
|
1021
1115
|
}
|
|
1022
1116
|
}
|
|
1023
1117
|
export function resolveModule(raw) {
|
|
1024
|
-
_synVarCounter = 0;
|
|
1025
1118
|
precomputeFieldTypes(raw.typeDecls);
|
|
1026
1119
|
const pureFns = computePureFns(raw.functions);
|
|
1027
1120
|
// Pre-compute function parameter and return types
|
|
@@ -1032,7 +1125,7 @@ export function resolveModule(raw) {
|
|
|
1032
1125
|
fnParams.set(fn.name, fn.params.map(p => resolveTsType(p.tsType, overrides, p.name)));
|
|
1033
1126
|
fnReturns.set(fn.name, resolveTsType(fn.returnType, overrides, "\\result"));
|
|
1034
1127
|
}
|
|
1035
|
-
const emptyCtx = { env: null, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: { kind: "int" }, pureFns, fnParams, fnReturns, inSpec: false, inLambda: false,
|
|
1128
|
+
const emptyCtx = { env: null, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: { kind: "int" }, pureFns, fnParams, fnReturns, inSpec: false, inLambda: false, narrowedPaths: [], narrowedIndices: [] };
|
|
1036
1129
|
const constants = (raw.constants ?? []).map(c => ({
|
|
1037
1130
|
name: c.name,
|
|
1038
1131
|
ty: parseTsType(c.tsType),
|