lemmascript 0.3.2 → 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 +5 -2
- package/package.json +3 -2
- package/tools/dist/dafny-commands.js +31 -14
- package/tools/dist/dafny-emit.js +63 -17
- package/tools/dist/extract.js +130 -21
- package/tools/dist/lean-emit.js +55 -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 +370 -194
- package/tools/dist/specparser.js +12 -2
- package/tools/dist/transform.js +337 -318
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,15 +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
|
-
*
|
|
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 `&&`. */
|
|
115
66
|
function detectOptionalCheck(cond, ctx) {
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}
|
|
118
77
|
// Identify the expression being checked against undefined
|
|
119
78
|
let optExpr = null;
|
|
120
79
|
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
@@ -123,24 +82,112 @@ function detectOptionalCheck(cond, ctx) {
|
|
|
123
82
|
optExpr = cond.right;
|
|
124
83
|
if (!optExpr)
|
|
125
84
|
return null;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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);
|
|
129
93
|
if (!ty || ty.kind !== "optional")
|
|
130
94
|
return null;
|
|
131
|
-
return { varName:
|
|
95
|
+
return { varName: e.name, innerTy: ty.inner };
|
|
132
96
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
97
|
+
const resolved = resolveExpr(e, ctx);
|
|
98
|
+
if (resolved.ty.kind !== "optional")
|
|
99
|
+
return null;
|
|
100
|
+
return { varName: "", innerTy: resolved.ty.inner, fieldExpr: e };
|
|
101
|
+
}
|
|
102
|
+
/** Collect all optional narrowings from an early-return condition.
|
|
103
|
+
* Handles single checks (x === undefined) and compound || chains
|
|
104
|
+
* (x === undefined || y === undefined). */
|
|
105
|
+
function collectEarlyReturnNarrowings(cond, ctx) {
|
|
106
|
+
if (cond.kind === "binop" && cond.op === "||") {
|
|
107
|
+
return [...collectEarlyReturnNarrowings(cond.left, ctx), ...collectEarlyReturnNarrowings(cond.right, ctx)];
|
|
108
|
+
}
|
|
109
|
+
const narrowed = detectOptionalCheck(cond, ctx);
|
|
110
|
+
if (narrowed && !narrowed.inThen && !narrowed.fieldExpr) {
|
|
111
|
+
return [{ varName: narrowed.varName, innerTy: narrowed.innerTy }];
|
|
112
|
+
}
|
|
113
|
+
return [];
|
|
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] };
|
|
141
125
|
}
|
|
142
126
|
return null;
|
|
143
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
|
+
}
|
|
144
191
|
/** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
|
|
145
192
|
function isRefMutableInTS(ty) {
|
|
146
193
|
return ty.kind === "array" || ty.kind === "map" || ty.kind === "set";
|
|
@@ -317,6 +364,34 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
317
364
|
}
|
|
318
365
|
return { kind: "unknown" };
|
|
319
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
|
+
}
|
|
320
395
|
// ── Resolve expressions ──────────────────────────────────────
|
|
321
396
|
function resolveExpr(e, ctx) {
|
|
322
397
|
switch (e.kind) {
|
|
@@ -344,16 +419,12 @@ function resolveExpr(e, ctx) {
|
|
|
344
419
|
}
|
|
345
420
|
case "binop": {
|
|
346
421
|
let left = resolveExpr(e.left, ctx);
|
|
347
|
-
// && narrowing:
|
|
348
|
-
//
|
|
349
|
-
// 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.)
|
|
350
424
|
let rightCtx = ctx;
|
|
351
425
|
let rawRight = e.right;
|
|
352
|
-
if (e.op === "&&") {
|
|
353
|
-
|
|
354
|
-
if (narrowed && narrowed.inThen && !narrowed.fieldExpr) {
|
|
355
|
-
rightCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
|
|
356
|
-
}
|
|
426
|
+
if (e.op === "&&" || e.op === "==>") {
|
|
427
|
+
rightCtx = collectAndChainNarrowings(e.left, ctx);
|
|
357
428
|
}
|
|
358
429
|
let right = resolveExpr(rawRight, rightCtx);
|
|
359
430
|
if (e.op === "===" || e.op === "!==") {
|
|
@@ -389,49 +460,122 @@ function resolveExpr(e, ctx) {
|
|
|
389
460
|
fn.obj.ty.elem.kind === "user") {
|
|
390
461
|
argCtx = { ...ctx, returnTy: fn.obj.ty.elem };
|
|
391
462
|
}
|
|
392
|
-
|
|
393
|
-
|
|
463
|
+
// Propagate parameter types to arguments for record literal resolution
|
|
464
|
+
// (enables inline discriminated union construction in function arguments)
|
|
465
|
+
const paramTypes = fn.kind === "var" && ctx.fnParams.has(fn.name) ? ctx.fnParams.get(fn.name) : null;
|
|
466
|
+
const args = coerceCallArgs(rawArgs.map((a, i) => {
|
|
467
|
+
let aCtx = argCtx;
|
|
468
|
+
if (paramTypes && i < paramTypes.length && paramTypes[i].kind === "user") {
|
|
469
|
+
aCtx = { ...aCtx, returnTy: paramTypes[i] };
|
|
470
|
+
}
|
|
471
|
+
return resolveExpr(a, aCtx);
|
|
472
|
+
}), fn, ctx);
|
|
473
|
+
let ty = inferMethodReturnTy(fn, args, ctx);
|
|
474
|
+
// For same-file function calls, use the known return type
|
|
475
|
+
if (ty.kind === "unknown" && fn.kind === "var" && ctx.fnReturns.has(fn.name)) {
|
|
476
|
+
ty = ctx.fnReturns.get(fn.name);
|
|
477
|
+
}
|
|
394
478
|
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
|
|
395
479
|
}
|
|
396
480
|
case "index": {
|
|
397
481
|
const obj = resolveExpr(e.obj, ctx);
|
|
398
482
|
const idx = resolveExpr(e.idx, ctx);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
+
}
|
|
402
500
|
return { kind: "index", obj, idx, ty: idxTy };
|
|
403
501
|
}
|
|
404
502
|
case "field": {
|
|
405
503
|
const obj = resolveExpr(e.obj, ctx);
|
|
406
504
|
let isDiscriminant = false;
|
|
407
505
|
let ty = { kind: "unknown" };
|
|
408
|
-
|
|
409
|
-
|
|
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
|
+
}
|
|
410
516
|
}
|
|
411
|
-
|
|
412
|
-
|
|
517
|
+
if (ty.kind === "unknown") {
|
|
518
|
+
const lookup = lookupFieldTy(obj.ty, e.field, ctx);
|
|
519
|
+
ty = lookup.ty;
|
|
520
|
+
isDiscriminant = lookup.isDiscriminant;
|
|
413
521
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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;
|
|
422
553
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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";
|
|
431
571
|
}
|
|
572
|
+
chain.push({ kind: "call", args, ty: callTy, callKind });
|
|
573
|
+
stepInTy = callTy;
|
|
432
574
|
}
|
|
433
575
|
}
|
|
434
|
-
|
|
576
|
+
const finalTy = stepInTy;
|
|
577
|
+
const ty = finalTy.kind === "optional" ? finalTy : { kind: "optional", inner: finalTy };
|
|
578
|
+
return { kind: "optChain", obj, chain, ty };
|
|
435
579
|
}
|
|
436
580
|
case "record": {
|
|
437
581
|
const spread = e.spread ? resolveExpr(e.spread, ctx) : null;
|
|
@@ -442,8 +586,13 @@ function resolveExpr(e, ctx) {
|
|
|
442
586
|
// Clear returnTy for field values — it applies to THIS record, not nested ones
|
|
443
587
|
const fieldCtx = recordTy ? { ...ctx, returnTy: { kind: "unknown" } } : ctx;
|
|
444
588
|
const fields = e.fields.map(f => {
|
|
445
|
-
let value = resolveExpr(f.value, fieldCtx);
|
|
446
589
|
const fieldDecl = decl?.fields?.find(df => df.name === f.name);
|
|
590
|
+
// Propagate declared field type into context so nested records resolve
|
|
591
|
+
// their union variant correctly (e.g., { kind: 'Idle' } → EffectMode.Idle)
|
|
592
|
+
const valueCtx = (fieldDecl?.type?.kind === "user")
|
|
593
|
+
? { ...fieldCtx, returnTy: fieldDecl.type }
|
|
594
|
+
: fieldCtx;
|
|
595
|
+
let value = resolveExpr(f.value, valueCtx);
|
|
447
596
|
if (fieldDecl) {
|
|
448
597
|
const declTy = fieldDecl.type;
|
|
449
598
|
value = coerceStr(value, declTy);
|
|
@@ -498,56 +647,43 @@ function resolveExpr(e, ctx) {
|
|
|
498
647
|
}
|
|
499
648
|
case "conditional": {
|
|
500
649
|
const cond = resolveExpr(e.cond, ctx);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
narrowedVar = `_opt${_synVarCounter++}`;
|
|
514
|
-
rawThen = substituteRawExpr(e.then, e.cond, { kind: "var", name: narrowedVar });
|
|
515
|
-
thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
|
|
516
|
-
}
|
|
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);
|
|
656
|
+
let elseCtx = ctx;
|
|
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));
|
|
517
660
|
}
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
if (!
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
thenCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
|
|
527
|
-
if (narrowed.fieldExpr) {
|
|
528
|
-
narrowedExprResolved = narrowed.narrowedExpr ?? resolveExpr(narrowed.fieldExpr, ctx);
|
|
529
|
-
rawThen = substituteRawExpr(e.then, narrowed.fieldExpr, { kind: "var", name: narrowed.varName });
|
|
530
|
-
}
|
|
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));
|
|
531
669
|
}
|
|
532
670
|
}
|
|
533
|
-
|
|
534
|
-
|
|
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);
|
|
676
|
+
let else_ = resolveExpr(e.else, elseCtx);
|
|
535
677
|
then_ = coerceStr(then_, else_.ty);
|
|
536
678
|
else_ = coerceStr(else_, then_.ty);
|
|
537
679
|
let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
|
|
538
|
-
// When one branch is undefined, result is optional
|
|
539
680
|
if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
|
|
540
681
|
ty = { kind: "optional", inner: else_.ty };
|
|
541
682
|
}
|
|
542
683
|
else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
|
|
543
684
|
ty = { kind: "optional", inner: then_.ty };
|
|
544
685
|
}
|
|
545
|
-
|
|
546
|
-
const hasVoidBranch = then_.ty.kind === "void" || else_.ty.kind === "void";
|
|
547
|
-
if (narrowedExprResolved && hasVoidBranch && ty.kind !== "optional") {
|
|
548
|
-
ty = { kind: "optional", inner: ty };
|
|
549
|
-
}
|
|
550
|
-
return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
|
|
686
|
+
return { kind: "conditional", cond, then: then_, else: else_, ty };
|
|
551
687
|
}
|
|
552
688
|
case "emptyCollection": {
|
|
553
689
|
const ty = parseTsType(e.tsType);
|
|
@@ -580,17 +716,34 @@ function splitConj(e) {
|
|
|
580
716
|
function resolveBlock(stmts, ctx) {
|
|
581
717
|
const result = [];
|
|
582
718
|
let env = ctx.env;
|
|
719
|
+
let narrowedIndices = ctx.narrowedIndices;
|
|
583
720
|
for (const s of stmts) {
|
|
584
|
-
const
|
|
721
|
+
const currentCtx = { ...ctx, env, narrowedIndices };
|
|
722
|
+
const [typed, nextEnv] = resolveStmt(s, currentCtx);
|
|
585
723
|
result.push(typed);
|
|
586
724
|
env = nextEnv;
|
|
587
725
|
// Flow narrowing: if (x === undefined) { return } narrows x for rest of block.
|
|
726
|
+
// Also handles compound: if (x === undefined || y === undefined) { return }
|
|
588
727
|
// Field chains are excluded — resolve can't substitute in statement lists;
|
|
589
728
|
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
590
729
|
if (s.kind === "if" && s.then.length > 0 && s.then[s.then.length - 1].kind === "return" && s.else.length === 0) {
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
env = extend(env,
|
|
730
|
+
const narrowings = collectEarlyReturnNarrowings(s.cond, withEnv(ctx, env));
|
|
731
|
+
for (const n of narrowings) {
|
|
732
|
+
env = extend(env, n.varName, n.innerTy);
|
|
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;
|
|
594
747
|
}
|
|
595
748
|
}
|
|
596
749
|
}
|
|
@@ -600,7 +753,10 @@ function resolveStmt(s, ctx) {
|
|
|
600
753
|
switch (s.kind) {
|
|
601
754
|
case "let": {
|
|
602
755
|
const declTy = resolveTsType(s.tsType, ctx.overrides, s.name);
|
|
603
|
-
|
|
756
|
+
// Propagate declared type as returnTy so nested record expressions
|
|
757
|
+
// resolve union variants correctly (e.g., EffectState → mode: EffectMode → { kind: 'Idle' })
|
|
758
|
+
const initCtx = declTy.kind === "user" ? { ...ctx, returnTy: declTy } : ctx;
|
|
759
|
+
const init = coerceStr(resolveExpr(s.init, initCtx), declTy);
|
|
604
760
|
// Map indexing: TS says T, but access can fail → use Optional<T> from init
|
|
605
761
|
const ty = (declTy.kind !== "optional" && init.ty.kind === "optional") ? init.ty : declTy;
|
|
606
762
|
// const collections are mutable in value-semantics world (TS mutates in place, Dafny/Lean reassign)
|
|
@@ -629,30 +785,36 @@ function resolveStmt(s, ctx) {
|
|
|
629
785
|
return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
|
|
630
786
|
case "if": {
|
|
631
787
|
// Narrow optional<T> → T when checking !== undefined or undefined !==.
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
let thenCtx =
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
if (narrowed.inThen)
|
|
641
|
-
thenCtx = withEnv(ctx, env);
|
|
642
|
-
else
|
|
643
|
-
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));
|
|
644
796
|
}
|
|
645
|
-
|
|
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];
|
|
646
804
|
}
|
|
647
805
|
case "while": {
|
|
648
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));
|
|
649
811
|
return [{
|
|
650
812
|
kind: "while",
|
|
651
813
|
cond: resolveExpr(s.cond, ctx),
|
|
652
|
-
invariants:
|
|
814
|
+
invariants: resolvedInvariants,
|
|
653
815
|
decreases: s.decreases ? resolveSpec(s.decreases, whileSpecCtx) : null,
|
|
654
816
|
doneWith: s.doneWith ? resolveSpec(s.doneWith, whileSpecCtx) : null,
|
|
655
|
-
body: resolveBlock(s.body,
|
|
817
|
+
body: resolveBlock(s.body, bodyCtx),
|
|
656
818
|
}, ctx.env];
|
|
657
819
|
}
|
|
658
820
|
case "forof": {
|
|
@@ -833,6 +995,8 @@ function collectCallsStmts(stmts, fns, out) {
|
|
|
833
995
|
}
|
|
834
996
|
function computePureFns(functions) {
|
|
835
997
|
const allFnNames = new Set(functions.map(fn => fn.name));
|
|
998
|
+
// //@ pure functions are always considered pure — never taint callers
|
|
999
|
+
const forcePure = new Set(functions.filter(fn => fn.pure).map(fn => fn.name));
|
|
836
1000
|
// Build call graph: fn → set of same-file functions it calls
|
|
837
1001
|
const callGraph = new Map();
|
|
838
1002
|
for (const fn of functions) {
|
|
@@ -840,8 +1004,8 @@ function computePureFns(functions) {
|
|
|
840
1004
|
collectCallsStmts(fn.body, allFnNames, calls);
|
|
841
1005
|
callGraph.set(fn.name, calls);
|
|
842
1006
|
}
|
|
843
|
-
// Seed: syntactically non-pure functions
|
|
844
|
-
const nonPure = new Set(functions.filter(fn => !isSyntacticallyPure(fn.body)).map(fn => fn.name));
|
|
1007
|
+
// Seed: syntactically non-pure functions (skip //@ pure)
|
|
1008
|
+
const nonPure = new Set(functions.filter(fn => !forcePure.has(fn.name) && !isSyntacticallyPure(fn.body)).map(fn => fn.name));
|
|
845
1009
|
// Build reverse graph: fn → set of functions that call it
|
|
846
1010
|
const callers = new Map();
|
|
847
1011
|
for (const name of allFnNames)
|
|
@@ -850,12 +1014,12 @@ function computePureFns(functions) {
|
|
|
850
1014
|
for (const callee of callees)
|
|
851
1015
|
callers.get(callee).add(caller);
|
|
852
1016
|
}
|
|
853
|
-
// Propagate impurity through reverse call graph
|
|
1017
|
+
// Propagate impurity through reverse call graph (skip //@ pure)
|
|
854
1018
|
const worklist = [...nonPure];
|
|
855
1019
|
while (worklist.length > 0) {
|
|
856
1020
|
const fn = worklist.pop();
|
|
857
1021
|
for (const caller of callers.get(fn) ?? []) {
|
|
858
|
-
if (!nonPure.has(caller)) {
|
|
1022
|
+
if (!nonPure.has(caller) && !forcePure.has(caller)) {
|
|
859
1023
|
nonPure.add(caller);
|
|
860
1024
|
worklist.push(caller);
|
|
861
1025
|
}
|
|
@@ -888,7 +1052,7 @@ function containsReturn(stmts) {
|
|
|
888
1052
|
return false;
|
|
889
1053
|
}
|
|
890
1054
|
// ── Resolve function / module ────────────────────────────────
|
|
891
|
-
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
|
|
1055
|
+
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), fnReturns = new Map(), opts) {
|
|
892
1056
|
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
893
1057
|
const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
|
|
894
1058
|
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
@@ -897,7 +1061,7 @@ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
|
|
|
897
1061
|
env = extend(env, opts.thisBinding.name, opts.thisBinding.ty);
|
|
898
1062
|
for (const p of params)
|
|
899
1063
|
env = extend(env, p.name, p.ty);
|
|
900
|
-
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
|
|
1064
|
+
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, fnReturns, inSpec: false, inLambda: false, narrowedPaths: [], narrowedIndices: [] };
|
|
901
1065
|
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
902
1066
|
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
903
1067
|
// Apply type parameter constraints from //@ type T (==) annotations
|
|
@@ -905,21 +1069,31 @@ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
|
|
|
905
1069
|
const constraint = overrides.get(tp);
|
|
906
1070
|
return constraint ? `${tp}${constraint}` : tp;
|
|
907
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);
|
|
908
1080
|
return {
|
|
909
1081
|
name: fn.name, typeParams, params, returnTy,
|
|
910
|
-
requires:
|
|
911
|
-
ensures: resolveSpecs(fn.ensures,
|
|
1082
|
+
requires: resolvedRequires,
|
|
1083
|
+
ensures: resolveSpecs(fn.ensures, ensuresCtxNarrowed),
|
|
1084
|
+
decreases: fn.decreases ? resolveSpec(fn.decreases, requiresCtx) : null,
|
|
912
1085
|
isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
|
|
913
|
-
|
|
1086
|
+
forcePure: fn.pure,
|
|
1087
|
+
body: resolveBlock(fn.body, bodyCtx),
|
|
914
1088
|
};
|
|
915
1089
|
}
|
|
916
|
-
function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
|
|
1090
|
+
function resolveClass(cls, typeDecls, pureFns, fnParams = new Map(), fnReturns = new Map()) {
|
|
917
1091
|
const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
|
|
918
1092
|
// Create a synthetic record type for 'this' so field access resolves
|
|
919
1093
|
const thisType = { kind: "user", name: cls.name };
|
|
920
1094
|
const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType, type: parseTsType(f.tsType) })) };
|
|
921
1095
|
const allTypeDecls = [...typeDecls, thisDecl];
|
|
922
|
-
const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, {
|
|
1096
|
+
const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, fnReturns, {
|
|
923
1097
|
thisBinding: { name: "this", ty: thisType },
|
|
924
1098
|
forcePure: false, // class methods are never pure (they access this)
|
|
925
1099
|
}));
|
|
@@ -943,13 +1117,15 @@ function precomputeFieldTypes(typeDecls) {
|
|
|
943
1117
|
export function resolveModule(raw) {
|
|
944
1118
|
precomputeFieldTypes(raw.typeDecls);
|
|
945
1119
|
const pureFns = computePureFns(raw.functions);
|
|
946
|
-
// Pre-compute function parameter
|
|
1120
|
+
// Pre-compute function parameter and return types
|
|
947
1121
|
const fnParams = new Map();
|
|
1122
|
+
const fnReturns = new Map();
|
|
948
1123
|
for (const fn of raw.functions) {
|
|
949
1124
|
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
950
1125
|
fnParams.set(fn.name, fn.params.map(p => resolveTsType(p.tsType, overrides, p.name)));
|
|
1126
|
+
fnReturns.set(fn.name, resolveTsType(fn.returnType, overrides, "\\result"));
|
|
951
1127
|
}
|
|
952
|
-
const emptyCtx = { env: null, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: { kind: "int" }, pureFns, fnParams, 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: [] };
|
|
953
1129
|
const constants = (raw.constants ?? []).map(c => ({
|
|
954
1130
|
name: c.name,
|
|
955
1131
|
ty: parseTsType(c.tsType),
|
|
@@ -959,7 +1135,7 @@ export function resolveModule(raw) {
|
|
|
959
1135
|
file: raw.file,
|
|
960
1136
|
typeDecls: raw.typeDecls,
|
|
961
1137
|
constants,
|
|
962
|
-
functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns, fnParams)),
|
|
963
|
-
classes: (raw.classes ?? []).map(cls => resolveClass(cls, raw.typeDecls, pureFns, fnParams)),
|
|
1138
|
+
functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns, fnParams, fnReturns)),
|
|
1139
|
+
classes: (raw.classes ?? []).map(cls => resolveClass(cls, raw.typeDecls, pureFns, fnParams, fnReturns)),
|
|
964
1140
|
};
|
|
965
1141
|
}
|