lemmascript 0.3.3 → 0.5.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 +20 -13
- package/package.json +4 -1
- package/tools/dist/dafny-commands.js +31 -14
- package/tools/dist/dafny-emit.js +302 -17
- package/tools/dist/extract.js +1087 -181
- package/tools/dist/info-command.js +38 -0
- package/tools/dist/lean-emit.js +81 -5
- package/tools/dist/lsc.js +29 -9
- package/tools/dist/narrow.js +932 -0
- package/tools/dist/peephole.js +451 -0
- package/tools/dist/resolve.js +680 -258
- package/tools/dist/specparser.js +18 -2
- package/tools/dist/transform.js +597 -441
- package/tools/dist/types.js +128 -69
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,30 @@ function lookup(env, name) {
|
|
|
80
14
|
function extend(env, name, ty) {
|
|
81
15
|
return { name, ty, parent: env };
|
|
82
16
|
}
|
|
17
|
+
function envKeys(env) {
|
|
18
|
+
const out = [];
|
|
19
|
+
let e = env;
|
|
20
|
+
while (e) {
|
|
21
|
+
out.push(e.name);
|
|
22
|
+
e = e.parent;
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function asRawAccessPath(e) {
|
|
27
|
+
if (e.kind === "var")
|
|
28
|
+
return { rootVar: e.name, fields: [] };
|
|
29
|
+
if (e.kind === "field") {
|
|
30
|
+
const inner = asRawAccessPath(e.obj);
|
|
31
|
+
if (!inner)
|
|
32
|
+
return null;
|
|
33
|
+
return { rootVar: inner.rootVar, fields: [...inner.fields, e.field] };
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function accessPathsEqual(a, b) {
|
|
38
|
+
return a.rootVar === b.rootVar && a.fields.length === b.fields.length &&
|
|
39
|
+
a.fields.every((f, i) => f === b.fields[i]);
|
|
40
|
+
}
|
|
83
41
|
function withEnv(ctx, env) {
|
|
84
42
|
return { ...ctx, env };
|
|
85
43
|
}
|
|
@@ -106,17 +64,62 @@ function wrapSome(value, optionalTy) {
|
|
|
106
64
|
args: [value], ty: optionalTy, callKind: "pure",
|
|
107
65
|
};
|
|
108
66
|
}
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
67
|
+
/** Find the synth array-union TypeDecl named `name` (discriminant `__isArray__`). */
|
|
68
|
+
function findSynthArrayUnion(name, typeDecls) {
|
|
69
|
+
const decl = typeDecls.find(d => d.name === name);
|
|
70
|
+
if (decl?.kind === "discriminated-union" && decl.discriminant === "__isArray__")
|
|
71
|
+
return decl;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/** Coerce `value` to `targetTy` at an assignment-position. Mirrors TS subtyping
|
|
75
|
+
* for the two upcast shapes LS synthesizes:
|
|
76
|
+
* - `T` into `optional<T>` slot → wrap with `Some(...)`
|
|
77
|
+
* - `T[]` into a synth `T[] | U` slot → wrap with `ArrayBranch(...)`
|
|
78
|
+
* - `U` into a synth `T[] | U` slot → wrap with `NonArrayBranch(...)`
|
|
79
|
+
* Returns `value` unchanged if no coercion applies (types already match,
|
|
80
|
+
* source is unknown, or no rule matches). */
|
|
81
|
+
function coerceToTargetTy(value, targetTy, typeDecls) {
|
|
82
|
+
if (value.ty.kind === "unknown" || value.ty.kind === "void")
|
|
83
|
+
return value;
|
|
84
|
+
if (targetTy.kind === "optional" && value.ty.kind !== "optional") {
|
|
85
|
+
return wrapSome(value, targetTy);
|
|
86
|
+
}
|
|
87
|
+
if (targetTy.kind === "user") {
|
|
88
|
+
const synth = findSynthArrayUnion(targetTy.name, typeDecls);
|
|
89
|
+
if (synth && synth.variants && synth.variants.length === 2) {
|
|
90
|
+
const arrVariant = synth.variants.find(v => v.name === "ArrayBranch");
|
|
91
|
+
const nonVariant = synth.variants.find(v => v.name === "NonArrayBranch");
|
|
92
|
+
if (value.ty.kind === "array" && arrVariant) {
|
|
93
|
+
return { kind: "call", fn: { kind: "var", name: "ArrayBranch", ty: targetTy },
|
|
94
|
+
args: [value], ty: targetTy, callKind: "pure" };
|
|
95
|
+
}
|
|
96
|
+
if (value.ty.kind !== "array" && nonVariant) {
|
|
97
|
+
return { kind: "call", fn: { kind: "var", name: "NonArrayBranch", ty: targetTy },
|
|
98
|
+
args: [value], ty: targetTy, callKind: "pure" };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
/** Detect optional checks: `v !== undefined` (positive narrows then-branch),
|
|
105
|
+
* `v === undefined` (negative narrows else-branch), or `!v` (equivalent to
|
|
106
|
+
* `=== undefined`).
|
|
107
|
+
* Returns:
|
|
108
|
+
* - simple var: `varName` set, `fieldExpr` unset
|
|
109
|
+
* - complex (field chain or call): `fieldExpr` set, `varName` empty
|
|
110
|
+
* - inThen: true for `!==` (truthy), false for `===` and `!v` (falsy).
|
|
111
|
+
* Does NOT recurse into `&&`. */
|
|
117
112
|
function detectOptionalCheck(cond, ctx) {
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
// `!v` where v is optional — same shape as `v === undefined` (inThen: false).
|
|
114
|
+
if (cond.kind === "unop" && cond.op === "!") {
|
|
115
|
+
const inner = classifyOptExpr(cond.expr, ctx);
|
|
116
|
+
return inner ? { ...inner, inThen: false } : null;
|
|
117
|
+
}
|
|
118
|
+
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
|
|
119
|
+
// Bare optional truthiness: `if (v)` where v: T | undefined — same as `v !== undefined`.
|
|
120
|
+
const inner = classifyOptExpr(cond, ctx);
|
|
121
|
+
return inner ? { ...inner, inThen: true } : null;
|
|
122
|
+
}
|
|
120
123
|
// Identify the expression being checked against undefined
|
|
121
124
|
let optExpr = null;
|
|
122
125
|
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
@@ -125,23 +128,28 @@ function detectOptionalCheck(cond, ctx) {
|
|
|
125
128
|
optExpr = cond.right;
|
|
126
129
|
if (!optExpr)
|
|
127
130
|
return null;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
const inner = classifyOptExpr(optExpr, ctx);
|
|
132
|
+
return inner ? { ...inner, inThen: cond.op === "!==" } : null;
|
|
133
|
+
}
|
|
134
|
+
/** Classify an expression as a simple var or field-chain optional, returning
|
|
135
|
+
* the shape needed by detectOptionalCheck (sans inThen). */
|
|
136
|
+
function classifyOptExpr(e, ctx) {
|
|
137
|
+
if (e.kind === "var") {
|
|
138
|
+
const ty = lookup(ctx.env, e.name);
|
|
131
139
|
if (!ty || ty.kind !== "optional")
|
|
132
140
|
return null;
|
|
133
|
-
return { varName:
|
|
141
|
+
return { varName: e.name, innerTy: ty.inner };
|
|
134
142
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
varName: synVar, innerTy: resolved.ty.inner, inThen: cond.op === "!==",
|
|
141
|
-
fieldExpr: optExpr, narrowedExpr: resolved,
|
|
142
|
-
};
|
|
143
|
+
if (e.kind === "result") {
|
|
144
|
+
const ty = lookup(ctx.env, "\\result");
|
|
145
|
+
if (!ty || ty.kind !== "optional")
|
|
146
|
+
return null;
|
|
147
|
+
return { varName: "\\result", innerTy: ty.inner };
|
|
143
148
|
}
|
|
144
|
-
|
|
149
|
+
const resolved = resolveExpr(e, ctx);
|
|
150
|
+
if (resolved.ty.kind !== "optional")
|
|
151
|
+
return null;
|
|
152
|
+
return { varName: "", innerTy: resolved.ty.inner, fieldExpr: e };
|
|
145
153
|
}
|
|
146
154
|
/** Collect all optional narrowings from an early-return condition.
|
|
147
155
|
* Handles single checks (x === undefined) and compound || chains
|
|
@@ -156,16 +164,155 @@ function collectEarlyReturnNarrowings(cond, ctx) {
|
|
|
156
164
|
}
|
|
157
165
|
return [];
|
|
158
166
|
}
|
|
167
|
+
/** TExpr → AccessPath. Counterpart to `asRawAccessPath` for resolved trees.
|
|
168
|
+
* Used by `extractInAtoms` when pulling atoms out of typed spec expressions. */
|
|
169
|
+
function asTExprAccessPath(e) {
|
|
170
|
+
if (e.kind === "var")
|
|
171
|
+
return { rootVar: e.name, fields: [] };
|
|
172
|
+
if (e.kind === "field") {
|
|
173
|
+
const inner = asTExprAccessPath(e.obj);
|
|
174
|
+
if (!inner)
|
|
175
|
+
return null;
|
|
176
|
+
return { rootVar: inner.rootVar, fields: [...inner.fields, e.field] };
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
/** Walk `e` collecting top-level `k in m` atoms where both sides are pure
|
|
181
|
+
* access paths and the right side is map-typed. Descends through `&&` only.
|
|
182
|
+
* Does NOT descend into `==>`, `||`, negation, `forall`, or `exists` — in
|
|
183
|
+
* those positions an atom is only conditionally known (or a premise, not a
|
|
184
|
+
* conclusion), so treating it as always-true in the enclosing scope would
|
|
185
|
+
* be unsound. */
|
|
186
|
+
function extractInAtoms(e) {
|
|
187
|
+
if (e.kind === "binop" && e.op === "in" && e.right.ty.kind === "map") {
|
|
188
|
+
const obj = asTExprAccessPath(e.right);
|
|
189
|
+
const idx = asTExprAccessPath(e.left);
|
|
190
|
+
if (obj && idx)
|
|
191
|
+
return [{ obj, idx }];
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
if (e.kind === "binop" && e.op === "&&") {
|
|
195
|
+
return [...extractInAtoms(e.left), ...extractInAtoms(e.right)];
|
|
196
|
+
}
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
/** Extract `k in m` atoms that hold when `e` is *false*. Currently only
|
|
200
|
+
* strips an outer `!` and hands the inner to `extractInAtoms`; that covers
|
|
201
|
+
* `if (!(k in m)) ...` for the else-branch and early-return patterns.
|
|
202
|
+
* De Morgan over `||` / nested `!(a && b)` not handled yet. */
|
|
203
|
+
function extractInAtomsNegated(e) {
|
|
204
|
+
if (e.kind === "unop" && e.op === "!")
|
|
205
|
+
return extractInAtoms(e.expr);
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
/** Extend a Ctx with `k in m` atoms. Deduplicates against existing atoms. */
|
|
209
|
+
function withInAtoms(ctx, atoms) {
|
|
210
|
+
if (atoms.length === 0)
|
|
211
|
+
return ctx;
|
|
212
|
+
const existing = ctx.narrowedIndices;
|
|
213
|
+
const added = [];
|
|
214
|
+
for (const a of atoms) {
|
|
215
|
+
if (!existing.some(e => accessPathsEqual(e.obj, a.obj) && accessPathsEqual(e.idx, a.idx))) {
|
|
216
|
+
added.push(a);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (added.length === 0)
|
|
220
|
+
return ctx;
|
|
221
|
+
return { ...ctx, narrowedIndices: [...existing, ...added] };
|
|
222
|
+
}
|
|
223
|
+
/** Walk an `&&` chain of `e !== undefined` checks, returning a Ctx with all
|
|
224
|
+
* narrowings applied. Earlier checks are in scope for later checks (so the
|
|
225
|
+
* right side of `&&` sees the left side's narrowings). */
|
|
226
|
+
function collectAndChainNarrowings(cond, ctx) {
|
|
227
|
+
if (cond.kind === "binop" && cond.op === "&&") {
|
|
228
|
+
const leftCtx = collectAndChainNarrowings(cond.left, ctx);
|
|
229
|
+
return collectAndChainNarrowings(cond.right, leftCtx);
|
|
230
|
+
}
|
|
231
|
+
const n = detectOptionalCheck(cond, ctx);
|
|
232
|
+
if (!n || !n.inThen)
|
|
233
|
+
return ctx;
|
|
234
|
+
if (!n.fieldExpr) {
|
|
235
|
+
return withEnv(ctx, extend(ctx.env, n.varName, n.innerTy));
|
|
236
|
+
}
|
|
237
|
+
const path = asRawAccessPath(n.fieldExpr);
|
|
238
|
+
if (path) {
|
|
239
|
+
return { ...ctx, narrowedPaths: [...ctx.narrowedPaths, { path, narrowedTy: n.innerTy }] };
|
|
240
|
+
}
|
|
241
|
+
return ctx;
|
|
242
|
+
}
|
|
159
243
|
/** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
|
|
160
244
|
function isRefMutableInTS(ty) {
|
|
161
245
|
return ty.kind === "array" || ty.kind === "map" || ty.kind === "set";
|
|
162
246
|
}
|
|
163
247
|
function findDecl(ctx, name) {
|
|
164
|
-
|
|
248
|
+
const direct = ctx.typeDecls.find(d => d.name === name);
|
|
249
|
+
if (direct)
|
|
250
|
+
return direct;
|
|
251
|
+
// Dotted names (e.g. `Agent.Info`, `Permission.Ruleset`): fall back to the
|
|
252
|
+
// last segment, so `//@ declare-type Info { ... }` matches a reference to
|
|
253
|
+
// `Agent.Info` without forcing the user to repeat the namespace.
|
|
254
|
+
const dotIdx = name.lastIndexOf(".");
|
|
255
|
+
if (dotIdx >= 0)
|
|
256
|
+
return ctx.typeDecls.find(d => d.name === name.slice(dotIdx + 1));
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
/** Expand alias-kind typeDecls when the alias target is structural (array,
|
|
260
|
+
* map, set, optional, or another user type). Primitive-typed aliases like
|
|
261
|
+
* `type TaskId = number` stay as `user("TaskId")` so the generated Dafny
|
|
262
|
+
* preserves the alias name. Recursive through compound types; cycle-safe. */
|
|
263
|
+
function expandAlias(ty, typeDecls, seen = new Set()) {
|
|
264
|
+
if (ty.kind === "user") {
|
|
265
|
+
if (seen.has(ty.name))
|
|
266
|
+
return ty;
|
|
267
|
+
let decl = typeDecls.find(d => d.name === ty.name);
|
|
268
|
+
if (!decl && ty.name.includes(".")) {
|
|
269
|
+
const tail = ty.name.slice(ty.name.lastIndexOf(".") + 1);
|
|
270
|
+
decl = typeDecls.find(d => d.name === tail);
|
|
271
|
+
}
|
|
272
|
+
if (decl?.kind === "alias" && decl.aliasOfTy) {
|
|
273
|
+
const target = decl.aliasOfTy;
|
|
274
|
+
if (target.kind === "array" || target.kind === "map" || target.kind === "set" || target.kind === "optional" || target.kind === "user") {
|
|
275
|
+
return expandAlias(target, typeDecls, new Set([...seen, ty.name]));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return ty;
|
|
279
|
+
}
|
|
280
|
+
if (ty.kind === "optional")
|
|
281
|
+
return { kind: "optional", inner: expandAlias(ty.inner, typeDecls, seen) };
|
|
282
|
+
if (ty.kind === "array")
|
|
283
|
+
return { kind: "array", elem: expandAlias(ty.elem, typeDecls, seen) };
|
|
284
|
+
if (ty.kind === "set")
|
|
285
|
+
return { kind: "set", elem: expandAlias(ty.elem, typeDecls, seen) };
|
|
286
|
+
if (ty.kind === "map")
|
|
287
|
+
return { kind: "map", key: expandAlias(ty.key, typeDecls, seen), value: expandAlias(ty.value, typeDecls, seen) };
|
|
288
|
+
return ty;
|
|
165
289
|
}
|
|
166
290
|
function getDiscriminant(ctx, typeName) {
|
|
167
291
|
return findDecl(ctx, typeName)?.discriminant;
|
|
168
292
|
}
|
|
293
|
+
/** A type ts-morph handed us that LemmaScript hasn't modeled: contains
|
|
294
|
+
* `unknown` (TS `any`), or a `user` type whose name isn't a known declaration
|
|
295
|
+
* (an opaque expanded union like `"AssistantMsg | ToolMsg"` that ts-morph
|
|
296
|
+
* produced by expanding an alias LS shadows via declare-type). Used by
|
|
297
|
+
* `case "let"` to decide when LS's own `init.ty` is the better source of
|
|
298
|
+
* structure. */
|
|
299
|
+
function isUnmodeledTy(ty, typeDecls) {
|
|
300
|
+
if (ty.kind === "unknown")
|
|
301
|
+
return true;
|
|
302
|
+
if (ty.kind === "optional")
|
|
303
|
+
return isUnmodeledTy(ty.inner, typeDecls);
|
|
304
|
+
if (ty.kind === "array")
|
|
305
|
+
return isUnmodeledTy(ty.elem, typeDecls);
|
|
306
|
+
if (ty.kind === "set")
|
|
307
|
+
return isUnmodeledTy(ty.elem, typeDecls);
|
|
308
|
+
if (ty.kind === "map")
|
|
309
|
+
return isUnmodeledTy(ty.key, typeDecls) || isUnmodeledTy(ty.value, typeDecls);
|
|
310
|
+
if (ty.kind === "user") {
|
|
311
|
+
const base = ty.name.includes("<") ? ty.name.slice(0, ty.name.indexOf("<")) : ty.name;
|
|
312
|
+
return !typeDecls.some(d => d.name === base);
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
169
316
|
/** Infer quantifier variable type from usage in body.
|
|
170
317
|
* If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
|
|
171
318
|
* return the collection's key type. Otherwise return null (default to int). */
|
|
@@ -232,8 +379,16 @@ function inferQuantVarType(varName, body, ctx) {
|
|
|
232
379
|
function classifyCall(fn, ctx) {
|
|
233
380
|
if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Math")
|
|
234
381
|
return "pure";
|
|
382
|
+
if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray")
|
|
383
|
+
return "pure";
|
|
235
384
|
if (fn.kind === "var" && (ctx.inSpec || ctx.inLambda) && ctx.pureFns.has(fn.name))
|
|
236
385
|
return "spec-pure";
|
|
386
|
+
// Bare-name `//@ extern` declarations are emitted as `function {:axiom}` —
|
|
387
|
+
// pure from the verifier's perspective. Classify them as pure so callers
|
|
388
|
+
// don't get lifted to statement-level binds (which would force lambdas to
|
|
389
|
+
// become multi-statement, illegal in Dafny).
|
|
390
|
+
if (fn.kind === "var" && ctx.externs.has(fn.name))
|
|
391
|
+
return "pure";
|
|
237
392
|
if (fn.kind === "var" && ctx.inSpec) {
|
|
238
393
|
// Not a known pure function — could be external (Lean-defined spec helper).
|
|
239
394
|
// Pass through as "pure" and let Lean catch any errors.
|
|
@@ -244,24 +399,56 @@ function classifyCall(fn, ctx) {
|
|
|
244
399
|
return "unknown";
|
|
245
400
|
}
|
|
246
401
|
// ── Call resolution helpers ─────────────────────────────────
|
|
247
|
-
/** Infer lambda param types from array method context (map, filter, etc.)
|
|
248
|
-
*
|
|
249
|
-
|
|
402
|
+
/** Infer lambda param types from array method context (map, filter, etc.)
|
|
403
|
+
* AND from function-typed parameters of named callees (e.g., a `Comparator =
|
|
404
|
+
* (a, b) => bool` parameter propagates `string, string` to the lambda's
|
|
405
|
+
* inline params). Returns updated rawArgs with inferred tsType. */
|
|
406
|
+
function tyToTsStr(ty) {
|
|
407
|
+
if (ty.kind === "user")
|
|
408
|
+
return ty.name;
|
|
409
|
+
if (ty.kind === "string")
|
|
410
|
+
return "string";
|
|
411
|
+
if (ty.kind === "int" || ty.kind === "nat")
|
|
412
|
+
return "number";
|
|
413
|
+
if (ty.kind === "bool")
|
|
414
|
+
return "boolean";
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
250
418
|
if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
251
|
-
["map", "filter", "every", "some", "find"].includes(fn.field) &&
|
|
419
|
+
["map", "filter", "every", "some", "find", "findLast", "findIndex"].includes(fn.field) &&
|
|
252
420
|
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
|
|
253
421
|
rawArgs[0].params.length >= 1 && !rawArgs[0].params[0].tsType) {
|
|
254
422
|
const elemTy = fn.obj.ty.elem;
|
|
255
|
-
const tsType = elemTy
|
|
256
|
-
: elemTy.kind === "string" ? "string"
|
|
257
|
-
: elemTy.kind === "int" || elemTy.kind === "nat" ? "number"
|
|
258
|
-
: elemTy.kind === "bool" ? "boolean" : undefined;
|
|
423
|
+
const tsType = tyToTsStr(elemTy);
|
|
259
424
|
if (tsType) {
|
|
260
425
|
const lam = rawArgs[0];
|
|
261
426
|
const updatedParams = [{ ...lam.params[0], tsType }, ...lam.params.slice(1)];
|
|
262
427
|
return [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
|
|
263
428
|
}
|
|
264
429
|
}
|
|
430
|
+
// Named-callee propagation: when an argument position expects a function
|
|
431
|
+
// type, infer the lambda's param types from that function type. Aliases
|
|
432
|
+
// (e.g., `Comparator`) are expanded via the typeDecls.
|
|
433
|
+
if (fn.kind === "var" && ctx?.fnParams.has(fn.name)) {
|
|
434
|
+
const paramTys = ctx.fnParams.get(fn.name);
|
|
435
|
+
return rawArgs.map((a, i) => {
|
|
436
|
+
if (a.kind !== "lambda" || i >= paramTys.length)
|
|
437
|
+
return a;
|
|
438
|
+
let pTy = paramTys[i];
|
|
439
|
+
if (pTy.kind === "user") {
|
|
440
|
+
const decl = ctx.typeDecls.find(d => d.name === pTy.name);
|
|
441
|
+
if (decl?.kind === "alias" && decl.aliasOfTy)
|
|
442
|
+
pTy = decl.aliasOfTy;
|
|
443
|
+
else if (decl?.kind === "alias" && decl.aliasOf)
|
|
444
|
+
pTy = parseTsType(decl.aliasOf);
|
|
445
|
+
}
|
|
446
|
+
if (pTy.kind !== "fn")
|
|
447
|
+
return a;
|
|
448
|
+
const updatedParams = a.params.map((p, idx) => p.tsType || idx >= pTy.params.length ? p : { ...p, tsType: tyToTsStr(pTy.params[idx]) });
|
|
449
|
+
return { ...a, params: updatedParams };
|
|
450
|
+
});
|
|
451
|
+
}
|
|
265
452
|
return rawArgs;
|
|
266
453
|
}
|
|
267
454
|
/** Coerce call arguments: string literals → user types, non-optional → Some, pad missing optional args. */
|
|
@@ -290,6 +477,11 @@ function coerceCallArgs(args, fn, ctx) {
|
|
|
290
477
|
function inferMethodReturnTy(fn, args, ctx) {
|
|
291
478
|
if (fn.kind !== "field")
|
|
292
479
|
return { kind: "unknown" };
|
|
480
|
+
// `Array.isArray(x)` always returns boolean. narrow.ts recognizes this call as
|
|
481
|
+
// a discriminator predicate when `x` has type of a synthesized array-union.
|
|
482
|
+
if (fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray") {
|
|
483
|
+
return { kind: "bool" };
|
|
484
|
+
}
|
|
293
485
|
const objTy = fn.obj.ty;
|
|
294
486
|
if (objTy.kind === "map") {
|
|
295
487
|
if (fn.field === "get")
|
|
@@ -312,12 +504,24 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
312
504
|
return { kind: "int" };
|
|
313
505
|
if (fn.field === "shift")
|
|
314
506
|
return objTy.elem;
|
|
507
|
+
if (fn.field === "pop")
|
|
508
|
+
return { kind: "optional", inner: objTy.elem };
|
|
315
509
|
if (fn.field === "push" || fn.field === "concat")
|
|
316
510
|
return objTy;
|
|
317
511
|
if (fn.field === "filter")
|
|
318
512
|
return objTy;
|
|
319
513
|
if (fn.field === "every" || fn.field === "some")
|
|
320
514
|
return { kind: "bool" };
|
|
515
|
+
if (fn.field === "find" || fn.field === "findLast")
|
|
516
|
+
return { kind: "optional", inner: objTy.elem };
|
|
517
|
+
if (fn.field === "findIndex")
|
|
518
|
+
return { kind: "int" };
|
|
519
|
+
if (fn.field === "flat" && objTy.elem.kind === "array")
|
|
520
|
+
return { kind: "array", elem: objTy.elem.elem };
|
|
521
|
+
if (fn.field === "slice")
|
|
522
|
+
return objTy;
|
|
523
|
+
if (fn.field === "join" && objTy.elem.kind === "string")
|
|
524
|
+
return { kind: "string" };
|
|
321
525
|
if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
|
|
322
526
|
const retTy = args[0].body.length > 0 && args[0].body[0].kind === "return"
|
|
323
527
|
? args[0].body[0].value.ty : { kind: "unknown" };
|
|
@@ -325,13 +529,45 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
325
529
|
}
|
|
326
530
|
}
|
|
327
531
|
else if (objTy.kind === "string") {
|
|
328
|
-
if (fn.field === "trim" || fn.field === "toLowerCase" || fn.field === "toUpperCase")
|
|
532
|
+
if (fn.field === "trim" || fn.field === "trimEnd" || fn.field === "trimStart" || fn.field === "toLowerCase" || fn.field === "toUpperCase")
|
|
329
533
|
return { kind: "string" };
|
|
330
|
-
if (fn.field === "
|
|
534
|
+
if (fn.field === "slice" || fn.field === "substring")
|
|
535
|
+
return { kind: "string" };
|
|
536
|
+
if (fn.field === "split")
|
|
537
|
+
return { kind: "array", elem: { kind: "string" } };
|
|
538
|
+
if (fn.field === "includes" || fn.field === "startsWith" || fn.field === "endsWith")
|
|
331
539
|
return { kind: "bool" };
|
|
332
540
|
}
|
|
333
541
|
return { kind: "unknown" };
|
|
334
542
|
}
|
|
543
|
+
/** Look up the type of `field` on `objTy`. Returns `unknown` if not found. */
|
|
544
|
+
function lookupFieldTy(objTy, field, ctx) {
|
|
545
|
+
if (field === "length" && (objTy.kind === "array" || objTy.kind === "string")) {
|
|
546
|
+
return { ty: { kind: "nat" }, isDiscriminant: false };
|
|
547
|
+
}
|
|
548
|
+
if (field === "size" && (objTy.kind === "map" || objTy.kind === "set")) {
|
|
549
|
+
return { ty: { kind: "nat" }, isDiscriminant: false };
|
|
550
|
+
}
|
|
551
|
+
if (objTy.kind === "user") {
|
|
552
|
+
const baseTyName = objTy.name.includes("<") ? objTy.name.slice(0, objTy.name.indexOf("<")) : objTy.name;
|
|
553
|
+
const isDiscriminant = getDiscriminant(ctx, baseTyName) === field;
|
|
554
|
+
const decl = findDecl(ctx, baseTyName);
|
|
555
|
+
if (decl?.kind === "record") {
|
|
556
|
+
const f = decl.fields?.find(f => f.name === field);
|
|
557
|
+
if (f)
|
|
558
|
+
return { ty: f.type, isDiscriminant };
|
|
559
|
+
}
|
|
560
|
+
if (decl?.kind === "discriminated-union" && decl.variants) {
|
|
561
|
+
for (const variant of decl.variants) {
|
|
562
|
+
const f = variant.fields.find(f => f.name === field);
|
|
563
|
+
if (f)
|
|
564
|
+
return { ty: f.type, isDiscriminant };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return { ty: { kind: "unknown" }, isDiscriminant };
|
|
568
|
+
}
|
|
569
|
+
return { ty: { kind: "unknown" }, isDiscriminant: false };
|
|
570
|
+
}
|
|
335
571
|
// ── Resolve expressions ──────────────────────────────────────
|
|
336
572
|
function resolveExpr(e, ctx) {
|
|
337
573
|
switch (e.kind) {
|
|
@@ -359,16 +595,12 @@ function resolveExpr(e, ctx) {
|
|
|
359
595
|
}
|
|
360
596
|
case "binop": {
|
|
361
597
|
let left = resolveExpr(e.left, ctx);
|
|
362
|
-
// && narrowing:
|
|
363
|
-
//
|
|
364
|
-
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
598
|
+
// && and ==> narrowing: left-side optional checks narrow the right side.
|
|
599
|
+
// (For ==>, the premise is assumed in the conclusion — same principle.)
|
|
365
600
|
let rightCtx = ctx;
|
|
366
601
|
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
|
-
}
|
|
602
|
+
if (e.op === "&&" || e.op === "==>") {
|
|
603
|
+
rightCtx = collectAndChainNarrowings(e.left, ctx);
|
|
372
604
|
}
|
|
373
605
|
let right = resolveExpr(rawRight, rightCtx);
|
|
374
606
|
if (e.op === "===" || e.op === "!==") {
|
|
@@ -396,8 +628,21 @@ function resolveExpr(e, ctx) {
|
|
|
396
628
|
return { kind: "unop", op: e.op, expr, ty: e.op === "!" ? { kind: "bool" } : expr.ty };
|
|
397
629
|
}
|
|
398
630
|
case "call": {
|
|
631
|
+
// Extern dispatch: `NS.method(args)` where NS.method is declared via
|
|
632
|
+
// `//@ extern`. Rewrite into a flat-name call (`NS_method(args)`) so the
|
|
633
|
+
// rest of the pipeline sees an ordinary pure function. The extern's
|
|
634
|
+
// declaration is emitted alongside the file as `function {:axiom} ...`.
|
|
635
|
+
if (e.fn.kind === "field" && e.fn.obj.kind === "var") {
|
|
636
|
+
const qualified = `${e.fn.obj.name}.${e.fn.field}`;
|
|
637
|
+
const ext = ctx.externs.get(qualified);
|
|
638
|
+
if (ext) {
|
|
639
|
+
const args = e.args.map(a => resolveExpr(a, ctx));
|
|
640
|
+
const fn = { kind: "var", name: ext.flat, ty: { kind: "unknown" } };
|
|
641
|
+
return { kind: "call", fn, args, ty: ext.returnTy, callKind: "pure" };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
399
644
|
const fn = resolveExpr(e.fn, ctx);
|
|
400
|
-
const rawArgs = inferLambdaParamTypes(fn, e.args);
|
|
645
|
+
const rawArgs = inferLambdaParamTypes(fn, e.args, ctx);
|
|
401
646
|
// For .push() on a typed array, resolve args with element type context
|
|
402
647
|
let argCtx = ctx;
|
|
403
648
|
if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "push" &&
|
|
@@ -407,13 +652,20 @@ function resolveExpr(e, ctx) {
|
|
|
407
652
|
// Propagate parameter types to arguments for record literal resolution
|
|
408
653
|
// (enables inline discriminated union construction in function arguments)
|
|
409
654
|
const paramTypes = fn.kind === "var" && ctx.fnParams.has(fn.name) ? ctx.fnParams.get(fn.name) : null;
|
|
410
|
-
|
|
655
|
+
let args = coerceCallArgs(rawArgs.map((a, i) => {
|
|
411
656
|
let aCtx = argCtx;
|
|
412
657
|
if (paramTypes && i < paramTypes.length && paramTypes[i].kind === "user") {
|
|
413
658
|
aCtx = { ...aCtx, returnTy: paramTypes[i] };
|
|
414
659
|
}
|
|
415
660
|
return resolveExpr(a, aCtx);
|
|
416
661
|
}), fn, ctx);
|
|
662
|
+
// Array method `.with(i, v)`: coerce the value arg to the element type
|
|
663
|
+
// so `arr[i] = v` on `(T|null)[]` wraps `T` → `Some(T)` (and similarly
|
|
664
|
+
// for synth array-unions). Same shape as the record-field coercion
|
|
665
|
+
// below: assigning a narrower value into a wider slot.
|
|
666
|
+
if (fn.kind === "field" && fn.field === "with" && fn.obj.ty.kind === "array" && args.length === 2) {
|
|
667
|
+
args = [args[0], coerceToTargetTy(args[1], fn.obj.ty.elem, ctx.typeDecls)];
|
|
668
|
+
}
|
|
417
669
|
let ty = inferMethodReturnTy(fn, args, ctx);
|
|
418
670
|
// For same-file function calls, use the known return type
|
|
419
671
|
if (ty.kind === "unknown" && fn.kind === "var" && ctx.fnReturns.has(fn.name)) {
|
|
@@ -424,56 +676,126 @@ function resolveExpr(e, ctx) {
|
|
|
424
676
|
case "index": {
|
|
425
677
|
const obj = resolveExpr(e.obj, ctx);
|
|
426
678
|
const idx = resolveExpr(e.idx, ctx);
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
679
|
+
// Map bracket access: default to Option<V>. But if the enclosing scope has a
|
|
680
|
+
// known `k in m` atom matching (obj, idx) — from requires, assert, an enclosing
|
|
681
|
+
// `if (k in m)`, or a loop invariant — narrow to V. Parallels how `narrowedPaths`
|
|
682
|
+
// narrows `obj.field.field` under optional-undefined checks.
|
|
683
|
+
let idxTy;
|
|
684
|
+
if (obj.ty.kind === "array") {
|
|
685
|
+
idxTy = obj.ty.elem;
|
|
686
|
+
}
|
|
687
|
+
else if (obj.ty.kind === "map") {
|
|
688
|
+
const objPath = asTExprAccessPath(obj);
|
|
689
|
+
const idxPath = asTExprAccessPath(idx);
|
|
690
|
+
const narrowed = objPath && idxPath && ctx.narrowedIndices.some(n => accessPathsEqual(n.obj, objPath) && accessPathsEqual(n.idx, idxPath));
|
|
691
|
+
idxTy = narrowed ? obj.ty.value : { kind: "optional", inner: obj.ty.value };
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
idxTy = { kind: "unknown" };
|
|
695
|
+
}
|
|
430
696
|
return { kind: "index", obj, idx, ty: idxTy };
|
|
431
697
|
}
|
|
432
698
|
case "field": {
|
|
433
699
|
const obj = resolveExpr(e.obj, ctx);
|
|
434
700
|
let isDiscriminant = false;
|
|
435
701
|
let ty = { kind: "unknown" };
|
|
436
|
-
// Check narrowed
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
702
|
+
// Check narrowed path context (from conditional optional checks).
|
|
703
|
+
// Applies when the current field-access forms a pure access path AND
|
|
704
|
+
// that path is in the narrowedPaths list.
|
|
705
|
+
if (ctx.narrowedPaths.length > 0) {
|
|
706
|
+
const myPath = asRawAccessPath(e);
|
|
707
|
+
if (myPath) {
|
|
708
|
+
const np = ctx.narrowedPaths.find(n => accessPathsEqual(n.path, myPath));
|
|
709
|
+
if (np)
|
|
710
|
+
ty = np.narrowedTy;
|
|
711
|
+
}
|
|
444
712
|
}
|
|
445
|
-
|
|
446
|
-
|
|
713
|
+
if (ty.kind === "unknown") {
|
|
714
|
+
const lookup = lookupFieldTy(obj.ty, e.field, ctx);
|
|
715
|
+
ty = lookup.ty;
|
|
716
|
+
isDiscriminant = lookup.isDiscriminant;
|
|
447
717
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
718
|
+
return { kind: "field", obj, field: e.field, ty, isDiscriminant };
|
|
719
|
+
}
|
|
720
|
+
case "nullish": {
|
|
721
|
+
// left ?? right — result type is left's inner (when left is optional)
|
|
722
|
+
// or just left's type, unified with right's type.
|
|
723
|
+
const left = resolveExpr(e.left, ctx);
|
|
724
|
+
const right = resolveExpr(e.right, ctx);
|
|
725
|
+
const ty = left.ty.kind === "optional" ? left.ty.inner : left.ty;
|
|
726
|
+
return { kind: "nullish", left, right, ty };
|
|
727
|
+
}
|
|
728
|
+
case "optChain": {
|
|
729
|
+
// obj?.<chain> — obj has type Option<T>; we walk the chain stepping
|
|
730
|
+
// through types from T. The final result is Option<finalStepTy>
|
|
731
|
+
// (collapsed: if finalStepTy is already optional, we don't double-wrap).
|
|
732
|
+
// Narrow rewrites this to a someMatch with the chain applied to the binder.
|
|
733
|
+
const obj = resolveExpr(e.obj, ctx);
|
|
734
|
+
let stepInTy = obj.ty.kind === "optional" ? obj.ty.inner : obj.ty;
|
|
735
|
+
const chain = [];
|
|
736
|
+
for (const step of e.chain) {
|
|
737
|
+
if (step.kind === "field") {
|
|
738
|
+
const fieldTy = lookupFieldTy(stepInTy, step.name, ctx).ty;
|
|
739
|
+
chain.push({ kind: "field", name: step.name, ty: fieldTy });
|
|
740
|
+
stepInTy = fieldTy;
|
|
458
741
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
742
|
+
else if (step.kind === "index") {
|
|
743
|
+
const idx = resolveExpr(step.idx, ctx);
|
|
744
|
+
const idxTy = stepInTy.kind === "array" ? stepInTy.elem
|
|
745
|
+
: stepInTy.kind === "map" ? { kind: "optional", inner: stepInTy.value }
|
|
746
|
+
: { kind: "unknown" };
|
|
747
|
+
chain.push({ kind: "index", idx, ty: idxTy });
|
|
748
|
+
stepInTy = idxTy;
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
// call: prev step yielded a callable (typically a method via field).
|
|
752
|
+
// Build a fake fn TExpr from prev steps to reuse inferMethodReturnTy
|
|
753
|
+
// and inferLambdaParamTypes — without the latter, a lambda arg buried
|
|
754
|
+
// inside `obj?.filter(r => ...)` gets `int`-typed params instead of
|
|
755
|
+
// the array's element type.
|
|
756
|
+
const lastField = chain.length > 0 && chain[chain.length - 1].kind === "field"
|
|
757
|
+
? chain[chain.length - 1] : null;
|
|
758
|
+
let callTy = { kind: "unknown" };
|
|
759
|
+
let callKind = "unknown";
|
|
760
|
+
let rawArgs = step.args;
|
|
761
|
+
if (lastField) {
|
|
762
|
+
const priorInTy = chain.length >= 2 ? chain[chain.length - 2].ty
|
|
763
|
+
: (obj.ty.kind === "optional" ? obj.ty.inner : obj.ty);
|
|
764
|
+
const fakeObj = { kind: "var", name: "_chain_recv", ty: priorInTy };
|
|
765
|
+
const fakeFn = { kind: "field", obj: fakeObj, field: lastField.name, ty: lastField.ty };
|
|
766
|
+
rawArgs = inferLambdaParamTypes(fakeFn, rawArgs);
|
|
767
|
+
const args = rawArgs.map(a => resolveExpr(a, ctx));
|
|
768
|
+
callTy = inferMethodReturnTy(fakeFn, args, ctx);
|
|
769
|
+
callKind = "method";
|
|
770
|
+
chain.push({ kind: "call", args, ty: callTy, callKind });
|
|
771
|
+
stepInTy = callTy;
|
|
772
|
+
continue;
|
|
467
773
|
}
|
|
774
|
+
const args = rawArgs.map(a => resolveExpr(a, ctx));
|
|
775
|
+
chain.push({ kind: "call", args, ty: callTy, callKind });
|
|
776
|
+
stepInTy = callTy;
|
|
468
777
|
}
|
|
469
778
|
}
|
|
470
|
-
|
|
779
|
+
const finalTy = stepInTy;
|
|
780
|
+
const ty = finalTy.kind === "optional" ? finalTy : { kind: "optional", inner: finalTy };
|
|
781
|
+
return { kind: "optChain", obj, chain, ty };
|
|
471
782
|
}
|
|
472
783
|
case "record": {
|
|
473
784
|
const spread = e.spread ? resolveExpr(e.spread, ctx) : null;
|
|
474
785
|
const ty = spread ? spread.ty : { kind: "unknown" };
|
|
475
|
-
//
|
|
476
|
-
|
|
786
|
+
// Record literal in map-typed context (e.g. `const M: Record<string, V> = {a: ...}`):
|
|
787
|
+
// attach the map type so transform/emit can produce a map literal.
|
|
788
|
+
if (!spread && ctx.returnTy.kind === "map") {
|
|
789
|
+
const mapTy = ctx.returnTy;
|
|
790
|
+
const fieldCtx = { ...ctx, returnTy: mapTy.value };
|
|
791
|
+
const fields = e.fields.map(f => ({ name: f.name, value: resolveExpr(f.value, fieldCtx) }));
|
|
792
|
+
return { kind: "record", spread: null, fields, ty: mapTy };
|
|
793
|
+
}
|
|
794
|
+
// Infer record type: from spread, or from return type context. Unwrap
|
|
795
|
+
// an outer Optional when looking at returnTy — `return {...} : null`
|
|
796
|
+
// has ctx.returnTy = Option<T>, but the record literal's natural type is T.
|
|
797
|
+
const returnTyUnwrapped = ctx.returnTy.kind === "optional" ? ctx.returnTy.inner : ctx.returnTy;
|
|
798
|
+
const recordTy = ty.kind === "user" ? ty : returnTyUnwrapped.kind === "user" ? returnTyUnwrapped : null;
|
|
477
799
|
const decl = recordTy ? ctx.typeDecls.find(d => d.name === recordTy.name && d.kind === "record") : undefined;
|
|
478
800
|
// Clear returnTy for field values — it applies to THIS record, not nested ones
|
|
479
801
|
const fieldCtx = recordTy ? { ...ctx, returnTy: { kind: "unknown" } } : ctx;
|
|
@@ -492,19 +814,22 @@ function resolveExpr(e, ctx) {
|
|
|
492
814
|
if (value.kind === "record" && value.fields.length === 0 && !value.spread && declTy.kind === "map") {
|
|
493
815
|
value = { kind: "arrayLiteral", elems: [], ty: declTy };
|
|
494
816
|
}
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
817
|
+
// Assignment-position upcasts: T → Option<T>, T[] → ArrayBranch(T[]),
|
|
818
|
+
// U → NonArrayBranch(U). Handles both optional fields and fields
|
|
819
|
+
// typed as a synth array-union (`T[] | U`).
|
|
820
|
+
value = coerceToTargetTy(value, declTy, ctx.typeDecls);
|
|
499
821
|
}
|
|
500
822
|
return { name: f.name, value };
|
|
501
823
|
});
|
|
502
824
|
return { kind: "record", spread, fields, ty: recordTy ?? ty };
|
|
503
825
|
}
|
|
504
826
|
case "result":
|
|
827
|
+
// \result desugars to a regular var so all the variable-narrowing
|
|
828
|
+
// machinery (env lookup, optional checks, path matching) just works.
|
|
829
|
+
// The env in ensuresCtx is pre-seeded with "\result" → returnTy.
|
|
505
830
|
if (!ctx.allowResult)
|
|
506
831
|
throw new Error("\\result is only valid in ensures");
|
|
507
|
-
return { kind: "result", ty: ctx.returnTy };
|
|
832
|
+
return { kind: "var", name: "\\result", ty: lookup(ctx.env, "\\result") ?? ctx.returnTy };
|
|
508
833
|
case "forall": {
|
|
509
834
|
const varTy = e.varType !== "int" ? parseTsType(e.varType)
|
|
510
835
|
: inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
|
|
@@ -539,87 +864,51 @@ function resolveExpr(e, ctx) {
|
|
|
539
864
|
}
|
|
540
865
|
case "conditional": {
|
|
541
866
|
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;
|
|
867
|
+
// Type narrowing for the then/else branches. Following TS, we narrow
|
|
868
|
+
// simple vars and any pure access path (`a.b.c.d`) — but not expressions
|
|
869
|
+
// with method calls or index ops (bind-first required).
|
|
870
|
+
// For &&-chains, all positive checks narrow the then-branch; earlier
|
|
871
|
+
// checks are in scope when resolving later ones.
|
|
872
|
+
let thenCtx = collectAndChainNarrowings(e.cond, ctx);
|
|
562
873
|
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
|
-
}
|
|
874
|
+
// Truthiness — cond itself is optional (`opt ? a : b`), only for simple vars.
|
|
875
|
+
if (cond.ty.kind === "optional" && e.cond.kind === "var") {
|
|
876
|
+
thenCtx = withEnv(thenCtx, extend(thenCtx.env, e.cond.name, cond.ty.inner));
|
|
877
|
+
}
|
|
878
|
+
// Single === undefined check narrows the else-branch.
|
|
879
|
+
const single = detectOptionalCheck(e.cond, ctx);
|
|
880
|
+
if (single && !single.inThen && !single.fieldExpr) {
|
|
881
|
+
elseCtx = withEnv(elseCtx, extend(elseCtx.env, single.varName, single.innerTy));
|
|
882
|
+
}
|
|
883
|
+
if (!single && e.cond.kind === "binop" && e.cond.op === "||") {
|
|
884
|
+
for (const n of collectEarlyReturnNarrowings(e.cond, ctx)) {
|
|
885
|
+
elseCtx = withEnv(elseCtx, extend(elseCtx.env, n.varName, n.innerTy));
|
|
604
886
|
}
|
|
605
887
|
}
|
|
606
|
-
|
|
888
|
+
// Map-index narrowing: `k in m` in the cond narrows the then-branch; `!(k in m)`
|
|
889
|
+
// narrows the else-branch. Parallel to the if-statement hook in resolveStmt.
|
|
890
|
+
thenCtx = withInAtoms(thenCtx, extractInAtoms(cond));
|
|
891
|
+
elseCtx = withInAtoms(elseCtx, extractInAtomsNegated(cond));
|
|
892
|
+
let then_ = resolveExpr(e.then, thenCtx);
|
|
607
893
|
let else_ = resolveExpr(e.else, elseCtx);
|
|
608
894
|
then_ = coerceStr(then_, else_.ty);
|
|
609
895
|
else_ = coerceStr(else_, then_.ty);
|
|
610
896
|
let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
|
|
611
|
-
// When one branch is undefined, result is optional
|
|
612
897
|
if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
|
|
613
898
|
ty = { kind: "optional", inner: else_.ty };
|
|
614
899
|
}
|
|
615
900
|
else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
|
|
616
901
|
ty = { kind: "optional", inner: then_.ty };
|
|
617
902
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
903
|
+
else if (then_.ty.kind === "optional" && else_.ty.kind !== "optional" && else_.ty.kind !== "unknown") {
|
|
904
|
+
// Asymmetric optional: one branch returns Option<T>, the other returns T.
|
|
905
|
+
// Widen to Option<T> so callers/return-coercion see the wider type.
|
|
906
|
+
ty = then_.ty;
|
|
907
|
+
}
|
|
908
|
+
else if (else_.ty.kind === "optional" && then_.ty.kind !== "optional" && then_.ty.kind !== "unknown") {
|
|
909
|
+
ty = else_.ty;
|
|
621
910
|
}
|
|
622
|
-
return { kind: "conditional", cond, then: then_, else: else_, ty
|
|
911
|
+
return { kind: "conditional", cond, then: then_, else: else_, ty };
|
|
623
912
|
}
|
|
624
913
|
case "emptyCollection": {
|
|
625
914
|
const ty = parseTsType(e.tsType);
|
|
@@ -652,8 +941,10 @@ function splitConj(e) {
|
|
|
652
941
|
function resolveBlock(stmts, ctx) {
|
|
653
942
|
const result = [];
|
|
654
943
|
let env = ctx.env;
|
|
944
|
+
let narrowedIndices = ctx.narrowedIndices;
|
|
655
945
|
for (const s of stmts) {
|
|
656
|
-
const
|
|
946
|
+
const currentCtx = { ...ctx, env, narrowedIndices };
|
|
947
|
+
const [typed, nextEnv] = resolveStmt(s, currentCtx);
|
|
657
948
|
result.push(typed);
|
|
658
949
|
env = nextEnv;
|
|
659
950
|
// Flow narrowing: if (x === undefined) { return } narrows x for rest of block.
|
|
@@ -665,6 +956,20 @@ function resolveBlock(stmts, ctx) {
|
|
|
665
956
|
for (const n of narrowings) {
|
|
666
957
|
env = extend(env, n.varName, n.innerTy);
|
|
667
958
|
}
|
|
959
|
+
// Map-index narrowing: `if (!(k in m)) return;` means `k in m` holds in rest.
|
|
960
|
+
if (typed.kind === "if") {
|
|
961
|
+
const addedAtoms = extractInAtomsNegated(typed.cond);
|
|
962
|
+
if (addedAtoms.length > 0) {
|
|
963
|
+
narrowedIndices = withInAtoms({ ...ctx, narrowedIndices }, addedAtoms).narrowedIndices;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// Assert narrowing: `//@ assert k in m` adds atoms for the rest of the block.
|
|
968
|
+
if (typed.kind === "assert") {
|
|
969
|
+
const addedAtoms = extractInAtoms(typed.expr);
|
|
970
|
+
if (addedAtoms.length > 0) {
|
|
971
|
+
narrowedIndices = withInAtoms({ ...ctx, narrowedIndices }, addedAtoms).narrowedIndices;
|
|
972
|
+
}
|
|
668
973
|
}
|
|
669
974
|
}
|
|
670
975
|
return result;
|
|
@@ -672,20 +977,48 @@ function resolveBlock(stmts, ctx) {
|
|
|
672
977
|
function resolveStmt(s, ctx) {
|
|
673
978
|
switch (s.kind) {
|
|
674
979
|
case "let": {
|
|
675
|
-
|
|
980
|
+
// No source annotation → infer type from initializer (resolved first).
|
|
981
|
+
if (s.tsType === null) {
|
|
982
|
+
const init = resolveExpr(s.init, ctx);
|
|
983
|
+
const ty = init.ty;
|
|
984
|
+
const mutable = s.mutable || isRefMutableInTS(ty);
|
|
985
|
+
return [{ kind: "let", name: s.name, ty, mutable, init }, extend(ctx.env, s.name, ty)];
|
|
986
|
+
}
|
|
987
|
+
// expandAlias unwraps an array/collection alias (`type Board = number[]`)
|
|
988
|
+
// to its underlying type, so array methods / index-assignment on the
|
|
989
|
+
// local dispatch correctly (params get the same treatment, see makeParams).
|
|
990
|
+
const declTy = expandAlias(resolveTsType(s.tsType, ctx.overrides, s.name), ctx.typeDecls);
|
|
676
991
|
// Propagate declared type as returnTy so nested record expressions
|
|
677
992
|
// resolve union variants correctly (e.g., EffectState → mode: EffectMode → { kind: 'Idle' })
|
|
678
993
|
const initCtx = declTy.kind === "user" ? { ...ctx, returnTy: declTy } : ctx;
|
|
679
994
|
const init = coerceStr(resolveExpr(s.init, initCtx), declTy);
|
|
680
|
-
|
|
681
|
-
|
|
995
|
+
let ty;
|
|
996
|
+
if (isUnmodeledTy(declTy, ctx.typeDecls) && !isUnmodeledTy(init.ty, ctx.typeDecls)) {
|
|
997
|
+
// ts-morph's declared type is opaque to us (an expanded union it made
|
|
998
|
+
// by inlining an alias we shadow via declare-type, or any-laden), but
|
|
999
|
+
// LS resolved the initializer to something concrete. Take the structure
|
|
1000
|
+
// from `init.ty`, keeping only the optionality ts-morph reported.
|
|
1001
|
+
ty = declTy.kind === "optional" && init.ty.kind !== "optional"
|
|
1002
|
+
? { kind: "optional", inner: init.ty }
|
|
1003
|
+
: init.ty;
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
// Map indexing: TS says T, but access can fail → use Optional<T> from init
|
|
1007
|
+
ty = (declTy.kind !== "optional" && init.ty.kind === "optional") ? init.ty : declTy;
|
|
1008
|
+
}
|
|
682
1009
|
// const collections are mutable in value-semantics world (TS mutates in place, Dafny/Lean reassign)
|
|
683
1010
|
const mutable = s.mutable || isRefMutableInTS(ty);
|
|
684
1011
|
return [{ kind: "let", name: s.name, ty, mutable, init }, extend(ctx.env, s.name, ty)];
|
|
685
1012
|
}
|
|
686
1013
|
case "assign": {
|
|
687
1014
|
const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
|
|
688
|
-
|
|
1015
|
+
let value = coerceStr(resolveExpr(s.value, ctx), targetTy);
|
|
1016
|
+
// Auto-wrap non-optional value in Some when target is optional
|
|
1017
|
+
const isUndef = value.kind === "var" && value.name === "undefined";
|
|
1018
|
+
if (targetTy.kind === "optional" && value.ty.kind !== "optional" && value.ty.kind !== "unknown" && !isUndef) {
|
|
1019
|
+
value = wrapSome(value, targetTy);
|
|
1020
|
+
}
|
|
1021
|
+
return [{ kind: "assign", target: s.target, value }, ctx.env];
|
|
689
1022
|
}
|
|
690
1023
|
case "return": {
|
|
691
1024
|
let value = coerceStr(resolveExpr(s.value, ctx), ctx.returnTy);
|
|
@@ -705,30 +1038,36 @@ function resolveStmt(s, ctx) {
|
|
|
705
1038
|
return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
|
|
706
1039
|
case "if": {
|
|
707
1040
|
// 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);
|
|
1041
|
+
// For &&-chains, all positive optional checks narrow the then-branch;
|
|
1042
|
+
// earlier checks are in scope when resolving later ones.
|
|
1043
|
+
// Single-check === undefined narrows the else-branch.
|
|
1044
|
+
let thenCtx = collectAndChainNarrowings(s.cond, ctx);
|
|
1045
|
+
let elseCtx = ctx;
|
|
1046
|
+
const single = detectOptionalCheck(s.cond, ctx);
|
|
1047
|
+
if (single && !single.inThen && !single.fieldExpr) {
|
|
1048
|
+
elseCtx = withEnv(ctx, extend(ctx.env, single.varName, single.innerTy));
|
|
720
1049
|
}
|
|
721
|
-
|
|
1050
|
+
// Narrow map index access across `k in m` / `!(k in m)` in the cond:
|
|
1051
|
+
// positive atoms (from `k in m` or &&-chains containing it) → then-branch;
|
|
1052
|
+
// negated atoms (from `!(k in m)`) → else-branch.
|
|
1053
|
+
const resolvedCond = resolveExpr(s.cond, ctx);
|
|
1054
|
+
thenCtx = withInAtoms(thenCtx, extractInAtoms(resolvedCond));
|
|
1055
|
+
elseCtx = withInAtoms(elseCtx, extractInAtomsNegated(resolvedCond));
|
|
1056
|
+
return [{ kind: "if", cond: resolvedCond, then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
|
|
722
1057
|
}
|
|
723
1058
|
case "while": {
|
|
724
1059
|
const whileSpecCtx = { ...ctx, inSpec: true };
|
|
1060
|
+
const resolvedInvariants = resolveSpecs(s.invariants, whileSpecCtx);
|
|
1061
|
+
// Invariants hold at the top of the body, so any `k in m` atoms among them
|
|
1062
|
+
// narrow map index access in the body.
|
|
1063
|
+
const bodyCtx = withInAtoms(ctx, resolvedInvariants.flatMap(extractInAtoms));
|
|
725
1064
|
return [{
|
|
726
1065
|
kind: "while",
|
|
727
1066
|
cond: resolveExpr(s.cond, ctx),
|
|
728
|
-
invariants:
|
|
1067
|
+
invariants: resolvedInvariants,
|
|
729
1068
|
decreases: s.decreases ? resolveSpec(s.decreases, whileSpecCtx) : null,
|
|
730
1069
|
doneWith: s.doneWith ? resolveSpec(s.doneWith, whileSpecCtx) : null,
|
|
731
|
-
body: resolveBlock(s.body,
|
|
1070
|
+
body: resolveBlock(s.body, bodyCtx),
|
|
732
1071
|
}, ctx.env];
|
|
733
1072
|
}
|
|
734
1073
|
case "forof": {
|
|
@@ -791,7 +1130,7 @@ function resolveStmt(s, ctx) {
|
|
|
791
1130
|
case "assert": {
|
|
792
1131
|
const specCtx = { ...ctx, inSpec: true };
|
|
793
1132
|
const expr = resolveExpr(parseExpr(s.expr), specCtx);
|
|
794
|
-
return [{ kind: "assert", expr }, ctx.env];
|
|
1133
|
+
return [{ kind: "assert", expr, assumed: s.assumed }, ctx.env];
|
|
795
1134
|
}
|
|
796
1135
|
}
|
|
797
1136
|
}
|
|
@@ -966,40 +1305,52 @@ function containsReturn(stmts) {
|
|
|
966
1305
|
return false;
|
|
967
1306
|
}
|
|
968
1307
|
// ── Resolve function / module ────────────────────────────────
|
|
969
|
-
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), fnReturns = new Map(), opts) {
|
|
1308
|
+
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), fnReturns = new Map(), externs = new Map(), moduleConstants = new Map(), opts) {
|
|
970
1309
|
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
971
|
-
const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
|
|
972
|
-
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
1310
|
+
const params = fn.params.map(p => ({ name: p.name, ty: expandAlias(resolveTsType(p.tsType, overrides, p.name), typeDecls) }));
|
|
1311
|
+
const returnTy = expandAlias(resolveTsType(fn.returnType, overrides, "\\result"), typeDecls);
|
|
973
1312
|
let env = null;
|
|
1313
|
+
// Module-level constants are in scope for every function body. Added before
|
|
1314
|
+
// params so a param named the same as a const would shadow it (param wins).
|
|
1315
|
+
for (const [name, ty] of moduleConstants)
|
|
1316
|
+
env = extend(env, name, ty);
|
|
974
1317
|
if (opts?.thisBinding)
|
|
975
1318
|
env = extend(env, opts.thisBinding.name, opts.thisBinding.ty);
|
|
976
1319
|
for (const p of params)
|
|
977
1320
|
env = extend(env, p.name, p.ty);
|
|
978
|
-
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, fnReturns, inSpec: false, inLambda: false,
|
|
1321
|
+
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, fnReturns, externs, inSpec: false, inLambda: false, narrowedPaths: [], narrowedIndices: [] };
|
|
979
1322
|
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
980
|
-
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
1323
|
+
const ensuresCtx = { ...baseCtx, env: extend(env, "\\result", returnTy), allowResult: true, inSpec: true };
|
|
981
1324
|
// Apply type parameter constraints from //@ type T (==) annotations
|
|
982
1325
|
const typeParams = fn.typeParams.map(tp => {
|
|
983
1326
|
const constraint = overrides.get(tp);
|
|
984
1327
|
return constraint ? `${tp}${constraint}` : tp;
|
|
985
1328
|
});
|
|
1329
|
+
// Resolve requires first so we can extract any `k in m` atoms and seed them
|
|
1330
|
+
// into the body context and the ensures context — they hold for the whole
|
|
1331
|
+
// body (pure fns) and for post-state references in the ensures (map params
|
|
1332
|
+
// aren't mutated through their binding in the Dafny translation).
|
|
1333
|
+
const resolvedRequires = resolveSpecs(fn.requires, requiresCtx);
|
|
1334
|
+
const requiresAtoms = resolvedRequires.flatMap(extractInAtoms);
|
|
1335
|
+
const bodyCtx = withInAtoms(baseCtx, requiresAtoms);
|
|
1336
|
+
const ensuresCtxNarrowed = withInAtoms(ensuresCtx, requiresAtoms);
|
|
986
1337
|
return {
|
|
987
1338
|
name: fn.name, typeParams, params, returnTy,
|
|
988
|
-
requires:
|
|
989
|
-
ensures: resolveSpecs(fn.ensures,
|
|
1339
|
+
requires: resolvedRequires,
|
|
1340
|
+
ensures: resolveSpecs(fn.ensures, ensuresCtxNarrowed),
|
|
990
1341
|
decreases: fn.decreases ? resolveSpec(fn.decreases, requiresCtx) : null,
|
|
991
1342
|
isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
|
|
992
1343
|
forcePure: fn.pure,
|
|
993
|
-
body: resolveBlock(fn.body,
|
|
1344
|
+
body: resolveBlock(fn.body, bodyCtx),
|
|
994
1345
|
};
|
|
995
1346
|
}
|
|
996
|
-
function resolveClass(cls, typeDecls, pureFns, fnParams = new Map(), fnReturns = new Map()) {
|
|
1347
|
+
function resolveClass(cls, typeDecls, pureFns, fnParams = new Map(), fnReturns = new Map(), externs = new Map(), moduleConstants = new Map()) {
|
|
997
1348
|
const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
|
|
998
1349
|
// Create a synthetic record type for 'this' so field access resolves
|
|
999
1350
|
const thisType = { kind: "user", name: cls.name };
|
|
1000
1351
|
const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType, type: parseTsType(f.tsType) })) };
|
|
1001
1352
|
const allTypeDecls = [...typeDecls, thisDecl];
|
|
1002
|
-
const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, fnReturns, {
|
|
1353
|
+
const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, fnReturns, externs, moduleConstants, {
|
|
1003
1354
|
thisBinding: { name: "this", ty: thisType },
|
|
1004
1355
|
forcePure: false, // class methods are never pure (they access this)
|
|
1005
1356
|
}));
|
|
@@ -1008,6 +1359,22 @@ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map(), fnReturns =
|
|
|
1008
1359
|
/** Pre-compute Ty on all TypeDeclInfo fields/variants/aliases.
|
|
1009
1360
|
* Called once per module so consumers can read field.type instead of re-parsing tsType. */
|
|
1010
1361
|
function precomputeFieldTypes(typeDecls) {
|
|
1362
|
+
precomputeFieldTypesInner(typeDecls);
|
|
1363
|
+
// Expand alias references inside record/variant field types so downstream
|
|
1364
|
+
// code doesn't have to follow `user("Ruleset")` indirection at every lookup.
|
|
1365
|
+
for (const d of typeDecls) {
|
|
1366
|
+
if (d.fields)
|
|
1367
|
+
for (const f of d.fields)
|
|
1368
|
+
if (f.type)
|
|
1369
|
+
f.type = expandAlias(f.type, typeDecls);
|
|
1370
|
+
if (d.variants)
|
|
1371
|
+
for (const v of d.variants)
|
|
1372
|
+
for (const f of v.fields)
|
|
1373
|
+
if (f.type)
|
|
1374
|
+
f.type = expandAlias(f.type, typeDecls);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
function precomputeFieldTypesInner(typeDecls) {
|
|
1011
1378
|
for (const d of typeDecls) {
|
|
1012
1379
|
if (d.fields)
|
|
1013
1380
|
for (const f of d.fields)
|
|
@@ -1021,7 +1388,6 @@ function precomputeFieldTypes(typeDecls) {
|
|
|
1021
1388
|
}
|
|
1022
1389
|
}
|
|
1023
1390
|
export function resolveModule(raw) {
|
|
1024
|
-
_synVarCounter = 0;
|
|
1025
1391
|
precomputeFieldTypes(raw.typeDecls);
|
|
1026
1392
|
const pureFns = computePureFns(raw.functions);
|
|
1027
1393
|
// Pre-compute function parameter and return types
|
|
@@ -1029,20 +1395,76 @@ export function resolveModule(raw) {
|
|
|
1029
1395
|
const fnReturns = new Map();
|
|
1030
1396
|
for (const fn of raw.functions) {
|
|
1031
1397
|
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
1032
|
-
fnParams.set(fn.name, fn.params.map(p => resolveTsType(p.tsType, overrides, p.name)));
|
|
1033
|
-
fnReturns.set(fn.name, resolveTsType(fn.returnType, overrides, "\\result"));
|
|
1398
|
+
fnParams.set(fn.name, fn.params.map(p => expandAlias(resolveTsType(p.tsType, overrides, p.name), raw.typeDecls)));
|
|
1399
|
+
fnReturns.set(fn.name, expandAlias(resolveTsType(fn.returnType, overrides, "\\result"), raw.typeDecls));
|
|
1034
1400
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1401
|
+
// Externs: resolve param/return types once. For bare-name externs (no dot),
|
|
1402
|
+
// also register in fnReturns so ordinary `foo(args)` calls get the right
|
|
1403
|
+
// return type at resolution; dotted externs are handled in resolveExpr's
|
|
1404
|
+
// call case via the externs map directly.
|
|
1405
|
+
const externs = new Map();
|
|
1406
|
+
// First pass: register signatures so spec resolution (below) can reference
|
|
1407
|
+
// them — including the extern referring to itself, or specs that mention
|
|
1408
|
+
// sibling externs.
|
|
1409
|
+
for (const ext of raw.externs ?? []) {
|
|
1410
|
+
const params = ext.params.map(p => parseTsType(p.tsType));
|
|
1411
|
+
const returnTy = parseTsType(ext.returnType);
|
|
1412
|
+
externs.set(ext.qualified, { flat: ext.flat, params, returnTy });
|
|
1413
|
+
if (!ext.qualified.includes("."))
|
|
1414
|
+
fnReturns.set(ext.qualified, returnTy);
|
|
1415
|
+
}
|
|
1416
|
+
// Second pass: resolve the lifted `requires`/`ensures` strings in each
|
|
1417
|
+
// extern's own param scope. `\result` is in scope under `ensures`.
|
|
1418
|
+
const tExterns = (raw.externs ?? []).map(ext => {
|
|
1419
|
+
const sig = externs.get(ext.qualified);
|
|
1420
|
+
let env = null;
|
|
1421
|
+
for (let i = 0; i < ext.params.length; i++) {
|
|
1422
|
+
env = extend(env, ext.params[i].name, sig.params[i]);
|
|
1423
|
+
}
|
|
1424
|
+
const baseCtx = { env, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: sig.returnTy, pureFns, fnParams, fnReturns, externs, inSpec: true, inLambda: false, narrowedPaths: [], narrowedIndices: [] };
|
|
1425
|
+
const ensuresCtx = { ...baseCtx, env: extend(env, "\\result", sig.returnTy), allowResult: true };
|
|
1426
|
+
const requires = ext.requires.map(s => {
|
|
1427
|
+
try {
|
|
1428
|
+
return resolveSpec(s, baseCtx);
|
|
1429
|
+
}
|
|
1430
|
+
catch {
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
}).filter((e) => e !== null);
|
|
1434
|
+
const ensures = ext.ensures.map(s => {
|
|
1435
|
+
try {
|
|
1436
|
+
return resolveSpec(s, ensuresCtx);
|
|
1437
|
+
}
|
|
1438
|
+
catch {
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
}).filter((e) => e !== null);
|
|
1442
|
+
return {
|
|
1443
|
+
qualified: ext.qualified,
|
|
1444
|
+
flat: ext.flat,
|
|
1445
|
+
typeParams: ext.typeParams,
|
|
1446
|
+
params: ext.params.map((p, i) => ({ name: p.name, ty: sig.params[i] })),
|
|
1447
|
+
returnTy: sig.returnTy,
|
|
1448
|
+
requires,
|
|
1449
|
+
ensures,
|
|
1450
|
+
};
|
|
1451
|
+
});
|
|
1452
|
+
const emptyCtx = { env: null, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: { kind: "int" }, pureFns, fnParams, fnReturns, externs, inSpec: false, inLambda: false, narrowedPaths: [], narrowedIndices: [] };
|
|
1453
|
+
const constants = (raw.constants ?? []).map(c => {
|
|
1454
|
+
const ty = expandAlias(parseTsType(c.tsType), raw.typeDecls);
|
|
1455
|
+
// Propagate the declared type into the value's resolution context so that
|
|
1456
|
+
// record literals on map-typed constants (e.g. `Record<string, number>`)
|
|
1457
|
+
// get their `ty` set to `map<...>` rather than `user("...")`.
|
|
1458
|
+
const valueCtx = { ...emptyCtx, returnTy: ty };
|
|
1459
|
+
return { name: c.name, ty, value: resolveExpr(c.value, valueCtx) };
|
|
1460
|
+
});
|
|
1461
|
+
const moduleConstants = new Map(constants.map(c => [c.name, c.ty]));
|
|
1041
1462
|
return {
|
|
1042
1463
|
file: raw.file,
|
|
1043
1464
|
typeDecls: raw.typeDecls,
|
|
1465
|
+
externs: tExterns,
|
|
1044
1466
|
constants,
|
|
1045
|
-
functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns, fnParams, fnReturns)),
|
|
1046
|
-
classes: (raw.classes ?? []).map(cls => resolveClass(cls, raw.typeDecls, pureFns, fnParams, fnReturns)),
|
|
1467
|
+
functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns, fnParams, fnReturns, externs, moduleConstants)),
|
|
1468
|
+
classes: (raw.classes ?? []).map(cls => resolveClass(cls, raw.typeDecls, pureFns, fnParams, fnReturns, externs, moduleConstants)),
|
|
1047
1469
|
};
|
|
1048
1470
|
}
|