lemmascript 0.5.17 → 0.5.19
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 +1 -1
- package/package.json +1 -1
- package/tools/dist/autohavoc.js +2 -0
- package/tools/dist/builtins.js +124 -0
- package/tools/dist/condition-facts.js +364 -0
- package/tools/dist/dafny-emit.js +162 -35
- package/tools/dist/extract.js +108 -21
- package/tools/dist/info-command.js +68 -0
- package/tools/dist/ir.js +27 -7
- package/tools/dist/lean-emit.js +27 -16
- package/tools/dist/lsc.js +53 -5
- package/tools/dist/names.js +10 -6
- package/tools/dist/narrow.js +297 -678
- package/tools/dist/peephole.js +12 -94
- package/tools/dist/rawir.js +15 -1
- package/tools/dist/resolve.js +187 -205
- package/tools/dist/specparser.js +21 -17
- package/tools/dist/transform.js +298 -108
- package/tools/dist/typedecls.js +59 -0
- package/tools/dist/typedir.js +6 -0
package/tools/dist/narrow.js
CHANGED
|
@@ -3,19 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pipeline: resolve → narrow → transform → emit.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - `e !== undefined ? a : b` (ternary)
|
|
15
|
-
* - `e !== undefined && rest ? a : b` (&& in ternary; pure rest)
|
|
16
|
-
* - `opt ? a : b` (truthiness)
|
|
17
|
-
* - `path !== undefined [&& rest] ==> B` (spec implication narrowing)
|
|
18
|
-
* - `optChain(obj, field)` (`obj?.field` from extract)
|
|
6
|
+
* Positional drivers over condition-facts (DESIGN_LS_IN_LS.md §4): the
|
|
7
|
+
* *semantics* of conditions — what a check establishes, `&&`/`||`-chain
|
|
8
|
+
* analysis, binder minting, discriminant detection — live in
|
|
9
|
+
* `condition-facts.ts`; this pass owns *where* a condition sits (if
|
|
10
|
+
* statement, early return + rest consumption, ternary, implication, guard
|
|
11
|
+
* statement, conditional initializer, nullish/optional chains, discriminant
|
|
12
|
+
* chains) and rewrites each position into `someMatch` / `tagMatch` IR via
|
|
13
|
+
* the shared materializers.
|
|
19
14
|
*
|
|
20
15
|
* Following TS semantics, narrowing rules only fire for pure access paths
|
|
21
16
|
* (`var(x)` or `field(purePath, name)`). Complex scrutinees (call results,
|
|
@@ -32,82 +27,27 @@
|
|
|
32
27
|
* Walker shape: bottom-up over TExpr/TStmt. At each node, recurse children
|
|
33
28
|
* via the *Recurse* helpers, then try the rules in order. List-level rules
|
|
34
29
|
* (early-return, let-cond) run in `walkStmts` so they can consume the rest
|
|
35
|
-
* of the block.
|
|
30
|
+
* of the block. Rule order matters — see the comments at the two dispatch
|
|
31
|
+
* chains.
|
|
32
|
+
*
|
|
33
|
+
* State is explicit (§6.1): a `CondCtx` (type declarations + optChain
|
|
34
|
+
* binder counter) threads through the walk; no module-level state.
|
|
36
35
|
*/
|
|
36
|
+
import { isTerminatorKind } from "./typedir.js";
|
|
37
37
|
import { freshName } from "./names.js";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
let _ocCounter = 0;
|
|
41
|
-
/** Type declarations for this module. Set in narrowModule, used by the
|
|
42
|
-
* discriminant-narrowing rules to resolve `'key' in x` to a variant. */
|
|
43
|
-
let _typeDecls = [];
|
|
44
|
-
/** Detect optional checks: `e !== undefined`, `e === undefined`, or `!e` for a
|
|
45
|
-
* pure-access-path optional-typed e. `!e` is equivalent to `=== undefined`.
|
|
46
|
-
* Following TS, only pure access paths narrow; complex scrutinees return null. */
|
|
47
|
-
function parseOptionalCheck(cond) {
|
|
48
|
-
// `!e` where e is optional — a truthiness form: false iff e is absent OR its
|
|
49
|
-
// inner value is itself falsy (so `Some(0)`/`Some("")` count as falsy too).
|
|
50
|
-
if (cond.kind === "unop" && cond.op === "!" && cond.expr.ty.kind === "optional") {
|
|
51
|
-
const e = cond.expr;
|
|
52
|
-
const innerTy = cond.expr.ty.inner;
|
|
53
|
-
const hint = binderHintFor(e);
|
|
54
|
-
if (hint === null)
|
|
55
|
-
return null;
|
|
56
|
-
return { scrutinee: e, innerTy, negated: true, binderHint: freshName(hint), truthiness: true };
|
|
57
|
-
}
|
|
58
|
-
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
|
|
59
|
-
// Bare optional truthiness: `if (e)` where e: T | undefined — true iff e is
|
|
60
|
-
// present AND its inner value is truthy.
|
|
61
|
-
if (cond.ty.kind === "optional") {
|
|
62
|
-
const hint = binderHintFor(cond);
|
|
63
|
-
if (hint === null)
|
|
64
|
-
return null;
|
|
65
|
-
return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: freshName(hint), truthiness: true };
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
// Explicit `e === undefined` / `e !== undefined` — a pure presence check,
|
|
70
|
-
// independent of the inner value (so NOT a truthiness form).
|
|
71
|
-
let e = null;
|
|
72
|
-
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
73
|
-
e = cond.left;
|
|
74
|
-
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
75
|
-
e = cond.right;
|
|
76
|
-
if (!e || e.ty.kind !== "optional")
|
|
77
|
-
return null;
|
|
78
|
-
const hint = binderHintFor(e);
|
|
79
|
-
if (hint === null)
|
|
80
|
-
return null;
|
|
81
|
-
return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: freshName(hint), truthiness: false };
|
|
82
|
-
}
|
|
83
|
-
function binderHintFor(e) {
|
|
84
|
-
// Pure access paths: var(x) or field(purePath, name).
|
|
85
|
-
// Walks down to the var root, collecting field names. Returns
|
|
86
|
-
// `_root_field1_field2_..._val` (or `_root_val` for a bare var).
|
|
87
|
-
const fields = [];
|
|
88
|
-
let cur = e;
|
|
89
|
-
while (cur.kind === "field") {
|
|
90
|
-
fields.unshift(cur.field);
|
|
91
|
-
cur = cur.obj;
|
|
92
|
-
}
|
|
93
|
-
if (cur.kind !== "var")
|
|
94
|
-
return null;
|
|
95
|
-
// \result is stored as the IR var name "\\result"; sanitize for a valid identifier.
|
|
96
|
-
const root = cur.name === "\\result" ? "result" : cur.name;
|
|
97
|
-
return fields.length === 0 ? `_${root}_val` : `_${root}_${fields.join("_")}_val`;
|
|
98
|
-
}
|
|
99
|
-
// Aliased for code that historically called the simpler check.
|
|
100
|
-
const parseSimpleOptionalCheck = parseOptionalCheck;
|
|
38
|
+
import { builtinSpec } from "./builtins.js";
|
|
39
|
+
import { presentFact, leadingPresent, leadingIsArray, flattenOr, noneDetector, binderHintFor, binderHintForMapAccess, freshOcBinder, applyChain, restoreDiscriminantFlag, arrayBoundsCond, exprEqual, isArrayFact, typeofStringFact, variantFact, negVariantFact, presentMatchStmts, presentMatchExpr, } from "./condition-facts.js";
|
|
101
40
|
// ── Walkers ──────────────────────────────────────────────────
|
|
102
|
-
function walkExpr(e) {
|
|
103
|
-
const r = recurseExpr(e);
|
|
104
|
-
return ruleNullish(r) ?? ruleNullishIndex(r) ?? ruleOptChainIndex(r) ?? ruleOptChain(r) ?? ruleImplOptional(r) ?? ruleImplArrayIsArray(r) ?? ruleConditionalArrayIsArray(r) ?? ruleConditionalAndArrayIsArray(r) ?? ruleConditionalAndOptional(r) ?? ruleConditionalOptionalSimple(r) ?? ruleConditionalInMap(r) ?? ruleConditionalOptionalTruthy(r) ?? r;
|
|
41
|
+
function walkExpr(e, ctx) {
|
|
42
|
+
const r = recurseExpr(e, ctx);
|
|
43
|
+
return ruleNullish(r, ctx) ?? ruleNullishIndex(r) ?? ruleOptChainIndex(r) ?? ruleOptChain(r, ctx) ?? ruleImplOptional(r, ctx) ?? ruleImplArrayIsArray(r, ctx) ?? ruleConditionalArrayIsArray(r, ctx) ?? ruleConditionalAndArrayIsArray(r, ctx) ?? ruleConditionalAndOptional(r, ctx) ?? ruleConditionalOptionalSimple(r) ?? ruleConditionalInMap(r, ctx) ?? ruleConditionalOptionalTruthy(r) ?? r;
|
|
105
44
|
}
|
|
106
|
-
function recurseExpr(e) {
|
|
107
|
-
const re = walkExpr;
|
|
45
|
+
function recurseExpr(e, ctx) {
|
|
46
|
+
const re = (x) => walkExpr(x, ctx);
|
|
108
47
|
switch (e.kind) {
|
|
109
48
|
case "var":
|
|
110
49
|
case "num":
|
|
50
|
+
case "bigint":
|
|
111
51
|
case "str":
|
|
112
52
|
case "bool":
|
|
113
53
|
case "havoc":
|
|
@@ -120,7 +60,7 @@ function recurseExpr(e) {
|
|
|
120
60
|
case "record": return { ...e, spread: e.spread ? re(e.spread) : null,
|
|
121
61
|
fields: e.fields.map(f => ({ ...f, value: re(f.value) })) };
|
|
122
62
|
case "arrayLiteral": return { ...e, elems: e.elems.map(re) };
|
|
123
|
-
case "lambda": return { ...e, body: walkStmts(e.body) };
|
|
63
|
+
case "lambda": return { ...e, body: walkStmts(e.body, ctx) };
|
|
124
64
|
case "conditional": return { ...e, cond: re(e.cond), then: re(e.then), else: re(e.else) };
|
|
125
65
|
case "optChain": return { ...e, obj: re(e.obj),
|
|
126
66
|
chain: e.chain.map(s => s.kind === "call" ? { ...s, args: s.args.map(re) }
|
|
@@ -135,9 +75,9 @@ function recurseExpr(e) {
|
|
|
135
75
|
fallthrough: e.fallthrough ? re(e.fallthrough) : null };
|
|
136
76
|
}
|
|
137
77
|
}
|
|
138
|
-
function walkStmt(s) {
|
|
78
|
+
function walkStmt(s, ctx) {
|
|
139
79
|
// Recurse into children first, then try rules at this node.
|
|
140
|
-
const r = recurseStmt(s);
|
|
80
|
+
const r = recurseStmt(s, ctx);
|
|
141
81
|
// Optional narrowing fires before Array.isArray narrowing: in a chain like
|
|
142
82
|
// `next && Array.isArray(next.content)` the optional check must unwrap `next`
|
|
143
83
|
// *outside* the array match, since `next.content` is unreachable until then.
|
|
@@ -145,41 +85,41 @@ function walkStmt(s) {
|
|
|
145
85
|
// array rule fires; independent narrows commute, so the order is harmless.)
|
|
146
86
|
// && rules fire before the simple rule because they produce nested ifs whose
|
|
147
87
|
// inner shape doesn't match the simple rule directly.
|
|
148
|
-
return ruleIfAndOptional(r) ?? ruleIfAndArrayIsArray(r) ?? ruleIfOptionalSimple(r) ?? ruleExprStmtAndOptional(r) ?? ruleOptionalIndexBinding(r) ?? r;
|
|
88
|
+
return ruleIfAndOptional(r, ctx) ?? ruleIfAndArrayIsArray(r, ctx) ?? ruleIfOptionalSimple(r) ?? ruleExprStmtAndOptional(r, ctx) ?? ruleOptionalIndexBinding(r) ?? r;
|
|
149
89
|
}
|
|
150
|
-
function walkStmts(stmts) {
|
|
90
|
+
function walkStmts(stmts, ctx) {
|
|
151
91
|
const result = [];
|
|
152
92
|
for (let i = 0; i < stmts.length; i++) {
|
|
153
93
|
const s = stmts[i];
|
|
154
94
|
const rest = stmts.slice(i + 1);
|
|
155
95
|
// Discriminant rules consume a prefix of stmts; remaining is processed normally.
|
|
156
|
-
const tagged = ruleDiscriminantChain(stmts.slice(i)) ?? ruleDiscriminantNegEarlyReturn(stmts.slice(i));
|
|
96
|
+
const tagged = ruleDiscriminantChain(stmts.slice(i), ctx) ?? ruleDiscriminantNegEarlyReturn(stmts.slice(i), ctx);
|
|
157
97
|
if (tagged) {
|
|
158
|
-
result.push(walkStmt(tagged.stmt));
|
|
98
|
+
result.push(walkStmt(tagged.stmt, ctx));
|
|
159
99
|
i += tagged.consumed - 1;
|
|
160
100
|
continue;
|
|
161
101
|
}
|
|
162
|
-
const consumed = ruleEarlyReturnOrChain(s, rest) ?? ruleEarlyReturnConsume(s, rest) ?? ruleEarlyReturnOptChainCompare(s, rest);
|
|
102
|
+
const consumed = ruleEarlyReturnOrChain(s, rest, ctx) ?? ruleEarlyReturnConsume(s, rest) ?? ruleEarlyReturnOptChainCompare(s, rest, ctx);
|
|
163
103
|
if (consumed) {
|
|
164
|
-
result.push(walkStmt(consumed));
|
|
104
|
+
result.push(walkStmt(consumed, ctx));
|
|
165
105
|
return result;
|
|
166
106
|
}
|
|
167
107
|
// walkStmt first — narrow's expression rules may rewrite the let init from
|
|
168
108
|
// `conditional` to `someMatch`, in which case the let-cond desugar shouldn't fire.
|
|
169
|
-
const walked = walkStmt(s);
|
|
109
|
+
const walked = walkStmt(s, ctx);
|
|
170
110
|
const expanded = ruleLetCondAndOptional(walked);
|
|
171
111
|
if (expanded) {
|
|
172
112
|
for (const x of expanded)
|
|
173
|
-
result.push(walkStmt(x));
|
|
113
|
+
result.push(walkStmt(x, ctx));
|
|
174
114
|
continue;
|
|
175
115
|
}
|
|
176
116
|
result.push(walked);
|
|
177
117
|
}
|
|
178
118
|
return result;
|
|
179
119
|
}
|
|
180
|
-
function recurseStmt(s) {
|
|
181
|
-
const re = walkExpr;
|
|
182
|
-
const rs = walkStmts;
|
|
120
|
+
function recurseStmt(s, ctx) {
|
|
121
|
+
const re = (x) => walkExpr(x, ctx);
|
|
122
|
+
const rs = (x) => walkStmts(x, ctx);
|
|
183
123
|
switch (s.kind) {
|
|
184
124
|
case "let": return { ...s, init: re(s.init) };
|
|
185
125
|
case "assign": return { ...s, value: re(s.value) };
|
|
@@ -210,40 +150,36 @@ function recurseStmt(s) {
|
|
|
210
150
|
fallthrough: rs(s.fallthrough) };
|
|
211
151
|
}
|
|
212
152
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
153
|
+
function isTerminating(stmts) {
|
|
154
|
+
if (stmts.length === 0)
|
|
155
|
+
return false;
|
|
156
|
+
return isTerminatorKind(stmts[stmts.length - 1].kind);
|
|
157
|
+
}
|
|
158
|
+
// ── Presence drivers ────────────────────────────────────────
|
|
159
|
+
/** Driver: `if (e !== undefined) then else` — presence fact in if position.
|
|
160
|
+
* → `someMatch e { Some(_e_val) => then, None => else }`. Requires a
|
|
161
|
+
* non-empty Some branch. */
|
|
219
162
|
function ruleIfOptionalSimple(s) {
|
|
220
163
|
if (s.kind !== "if")
|
|
221
164
|
return null;
|
|
222
|
-
const check =
|
|
165
|
+
const check = presentFact(s.cond);
|
|
223
166
|
if (!check)
|
|
224
167
|
return null;
|
|
225
168
|
const someBody = check.negated ? s.else : s.then;
|
|
226
169
|
const noneBody = check.negated ? s.then : s.else;
|
|
227
170
|
if (someBody.length === 0)
|
|
228
171
|
return null;
|
|
229
|
-
return
|
|
230
|
-
kind: "someMatch",
|
|
231
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
232
|
-
binder: check.binderHint,
|
|
233
|
-
someBody: canBeFalsy(check) ? [{ kind: "if", cond: bound(check), then: someBody, else: noneBody }] : someBody,
|
|
234
|
-
noneBody,
|
|
235
|
-
};
|
|
172
|
+
return presentMatchStmts(check, someBody, noneBody);
|
|
236
173
|
}
|
|
237
|
-
/**
|
|
238
|
-
*
|
|
239
|
-
* Fires when the Some branch is empty
|
|
240
|
-
* block — pulling the continuation into the narrowed scope. */
|
|
174
|
+
/** Driver: `if (e === undefined) terminate; rest` — presence fact in
|
|
175
|
+
* early-return position, consuming the rest of the block into the
|
|
176
|
+
* narrowed scope. Fires when the Some branch is empty. */
|
|
241
177
|
function ruleEarlyReturnConsume(s, rest) {
|
|
242
178
|
if (s.kind !== "if")
|
|
243
179
|
return null;
|
|
244
180
|
if (rest.length === 0)
|
|
245
181
|
return null;
|
|
246
|
-
const check =
|
|
182
|
+
const check = presentFact(s.cond);
|
|
247
183
|
if (!check)
|
|
248
184
|
return null;
|
|
249
185
|
const someBranch = check.negated ? s.else : s.then;
|
|
@@ -252,58 +188,15 @@ function ruleEarlyReturnConsume(s, rest) {
|
|
|
252
188
|
return null;
|
|
253
189
|
if (!isTerminating(noneBranch))
|
|
254
190
|
return null;
|
|
255
|
-
return
|
|
256
|
-
kind: "someMatch",
|
|
257
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
258
|
-
binder: check.binderHint,
|
|
259
|
-
someBody: canBeFalsy(check) ? [{ kind: "if", cond: bound(check), then: rest, else: noneBranch }] : rest,
|
|
260
|
-
noneBody: noneBranch,
|
|
261
|
-
};
|
|
191
|
+
return presentMatchStmts(check, rest, noneBranch);
|
|
262
192
|
}
|
|
263
|
-
/**
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
function
|
|
270
|
-
// `x?.chain !== lit` — `undefined !== lit` is true when x is None.
|
|
271
|
-
if (leaf.kind === "binop" && leaf.op === "!==") {
|
|
272
|
-
const oc = leaf.left.kind === "optChain" ? leaf.left : leaf.right.kind === "optChain" ? leaf.right : null;
|
|
273
|
-
if (oc && oc.kind === "optChain" && oc.obj.ty.kind === "optional") {
|
|
274
|
-
const hint = binderHintFor(oc.obj);
|
|
275
|
-
if (hint === null)
|
|
276
|
-
return null;
|
|
277
|
-
const binder = freshName(hint);
|
|
278
|
-
const unwrapped = applyChain({ kind: "var", name: binder, ty: oc.obj.ty.inner }, oc.chain);
|
|
279
|
-
if (unwrapped.kind === "field" && unwrapped.obj.ty.kind === "user") {
|
|
280
|
-
const base = unwrapped.obj.ty.name.replace(/<.*/, "");
|
|
281
|
-
const decl = _typeDecls.find(d => d.name === base);
|
|
282
|
-
if (decl?.kind === "discriminated-union" && decl.discriminant === unwrapped.field)
|
|
283
|
-
unwrapped.isDiscriminant = true;
|
|
284
|
-
}
|
|
285
|
-
const lit = leaf.left === oc ? leaf.right : leaf.left;
|
|
286
|
-
return { scrutinee: oc.obj, innerTy: oc.obj.ty.inner, binder, residual: { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } } };
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// `!x` / `x === undefined`.
|
|
290
|
-
const chk = parseOptionalCheck(leaf);
|
|
291
|
-
if (chk && chk.negated) {
|
|
292
|
-
const residual = canBeFalsy(chk)
|
|
293
|
-
? { kind: "unop", op: "!", expr: { kind: "var", name: chk.binderHint, ty: chk.innerTy }, ty: { kind: "bool" } }
|
|
294
|
-
: null;
|
|
295
|
-
return { scrutinee: chk.scrutinee, innerTy: chk.innerTy, binder: chk.binderHint, residual };
|
|
296
|
-
}
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
/** Rule: `if (D1 || … || Dn) terminate; rest`. Each `Di` that detects some optional
|
|
300
|
-
* `x` is None (`!x`, `x === undefined`, `x?.chain !== lit`) narrows that `x` to Some
|
|
301
|
-
* across `rest`; the rest — value guards reading a narrowed `x` directly, plus the
|
|
302
|
-
* detectors' Some-case residuals — become a trailing early-return. Sound: reaching
|
|
303
|
-
* `rest` means every disjunct was false, so every detected optional is present.
|
|
304
|
-
* Covers `if (!x || x.f !== v) continue` / `if (x?.t !== 'm' || x.g) break`.
|
|
305
|
-
* Closes the resolve.ts:602 TODO ("|| narrowing"). */
|
|
306
|
-
function ruleEarlyReturnOrChain(s, rest) {
|
|
193
|
+
/** Driver: `if (D1 || … || Dn) terminate; rest` — De-Morgan position. Each `Di`
|
|
194
|
+
* that detects some optional `x` is None (`!x`, `x === undefined`,
|
|
195
|
+
* `x?.chain !== lit`) narrows that `x` to Some across `rest`; the rest —
|
|
196
|
+
* value guards reading a narrowed `x` directly, plus the detectors' Some-case
|
|
197
|
+
* residuals — become a trailing early-return. Sound: reaching `rest` means
|
|
198
|
+
* every disjunct was false, so every detected optional is present. */
|
|
199
|
+
function ruleEarlyReturnOrChain(s, rest, ctx) {
|
|
307
200
|
if (s.kind !== "if")
|
|
308
201
|
return null;
|
|
309
202
|
if (rest.length === 0)
|
|
@@ -319,7 +212,7 @@ function ruleEarlyReturnOrChain(s, rest) {
|
|
|
319
212
|
const residualLeaves = [];
|
|
320
213
|
const seen = new Set();
|
|
321
214
|
for (const leaf of leaves) {
|
|
322
|
-
const d =
|
|
215
|
+
const d = noneDetector(leaf, ctx);
|
|
323
216
|
if (!d) {
|
|
324
217
|
residualLeaves.push(leaf);
|
|
325
218
|
continue;
|
|
@@ -343,17 +236,15 @@ function ruleEarlyReturnOrChain(s, rest) {
|
|
|
343
236
|
}
|
|
344
237
|
return inner[0];
|
|
345
238
|
}
|
|
346
|
-
/**
|
|
347
|
-
* `opt?.chain` is `undefined` when `opt` is None, and
|
|
348
|
-
* true, so the None case takes the terminating branch —
|
|
349
|
-
* `rest` proves `opt` is Some. Rewrite to
|
|
239
|
+
/** Driver: `if (opt?.chain !== lit) terminate; rest` — bound-optional
|
|
240
|
+
* early-return. `opt?.chain` is `undefined` when `opt` is None, and
|
|
241
|
+
* `undefined !== lit` is true, so the None case takes the terminating branch —
|
|
242
|
+
* falling through to `rest` proves `opt` is Some. Rewrite to
|
|
350
243
|
* someMatch opt { Some(v) => [if (v.chain !== lit) terminate; rest]; None => terminate }
|
|
351
|
-
* narrowing `opt` to `v` across `rest`
|
|
352
|
-
*
|
|
353
|
-
* discriminant narrowing). Bound-optional companion to ruleEarlyReturnConsume,
|
|
354
|
-
* which handles only a bare presence check (`opt !== undefined`). Restricted to
|
|
244
|
+
* narrowing `opt` to `v` across `rest` and handing the now-non-optional inner
|
|
245
|
+
* guard to the ordinary rules (e.g. discriminant narrowing). Restricted to
|
|
355
246
|
* `!==` so the None case is guaranteed to terminate. */
|
|
356
|
-
function ruleEarlyReturnOptChainCompare(s, rest) {
|
|
247
|
+
function ruleEarlyReturnOptChainCompare(s, rest, ctx) {
|
|
357
248
|
if (s.kind !== "if")
|
|
358
249
|
return null;
|
|
359
250
|
if (rest.length === 0)
|
|
@@ -374,16 +265,7 @@ function ruleEarlyReturnOptChainCompare(s, rest) {
|
|
|
374
265
|
const binder = freshName(hint);
|
|
375
266
|
const binderVar = { kind: "var", name: binder, ty: innerTy };
|
|
376
267
|
const unwrapped = applyChain(binderVar, oc.chain);
|
|
377
|
-
|
|
378
|
-
// on a direct `x.disc`; restore it when the unwrapped access is the binder
|
|
379
|
-
// union's discriminant, so the inner guard feeds discriminant narrowing.
|
|
380
|
-
if (unwrapped.kind === "field" && unwrapped.obj.ty.kind === "user") {
|
|
381
|
-
const base = unwrapped.obj.ty.name.replace(/<.*/, "");
|
|
382
|
-
const decl = _typeDecls.find(d => d.name === base);
|
|
383
|
-
if (decl?.kind === "discriminated-union" && decl.discriminant === unwrapped.field) {
|
|
384
|
-
unwrapped.isDiscriminant = true;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
268
|
+
restoreDiscriminantFlag(unwrapped, ctx.decls);
|
|
387
269
|
const innerGuard = { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } };
|
|
388
270
|
// Keep `rest` as trailing statements (not an else branch) — `s.then` terminates,
|
|
389
271
|
// so `if (g) terminate; rest` ≡ `if (g) terminate else rest`, and the trailing
|
|
@@ -392,99 +274,37 @@ function ruleEarlyReturnOptChainCompare(s, rest) {
|
|
|
392
274
|
const someBody = [{ kind: "if", cond: innerGuard, then: s.then, else: [] }, ...rest];
|
|
393
275
|
return { kind: "someMatch", scrutinee: oc.obj, binder, binderTy: innerTy, someBody, noneBody: s.then };
|
|
394
276
|
}
|
|
395
|
-
/**
|
|
277
|
+
/** Driver (expression): `e !== undefined ? a : b` — presence fact in
|
|
278
|
+
* ternary position. */
|
|
396
279
|
function ruleConditionalOptionalSimple(e) {
|
|
397
280
|
if (e.kind !== "conditional")
|
|
398
281
|
return null;
|
|
399
|
-
const check =
|
|
282
|
+
const check = presentFact(e.cond);
|
|
400
283
|
if (!check)
|
|
401
284
|
return null;
|
|
402
285
|
const someBody = check.negated ? e.else : e.then;
|
|
403
286
|
const noneBody = check.negated ? e.then : e.else;
|
|
404
|
-
return
|
|
405
|
-
kind: "someMatch",
|
|
406
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
407
|
-
binder: check.binderHint,
|
|
408
|
-
someBody: canBeFalsy(check) ? { kind: "conditional", cond: bound(check), then: someBody, else: noneBody, ty: e.ty } : someBody,
|
|
409
|
-
noneBody,
|
|
410
|
-
ty: e.ty,
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
/** Rule (expression): `Array.isArray(x) ==> B` or `!Array.isArray(x) ==> B` —
|
|
414
|
-
* premise narrowing for spec implications. Mirrors `ruleImplOptional` but for
|
|
415
|
-
* synth array-union discriminators.
|
|
416
|
-
* → `tagMatch x { ArrayBranch => walkExpr(B), _ => true }` (or NonArrayBranch).
|
|
417
|
-
* The other variant becomes a vacuous-true fallthrough (the implication is
|
|
418
|
-
* trivially satisfied when the premise is false). */
|
|
419
|
-
function ruleImplArrayIsArray(e) {
|
|
420
|
-
if (e.kind !== "binop" || e.op !== "==>")
|
|
421
|
-
return null;
|
|
422
|
-
const pos = parseArrayIsArrayCall(e.left);
|
|
423
|
-
const neg = e.left.kind === "unop" && e.left.op === "!"
|
|
424
|
-
? parseArrayIsArrayCall(e.left.expr)
|
|
425
|
-
: null;
|
|
426
|
-
const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
|
|
427
|
-
if (!matched)
|
|
428
|
-
return null;
|
|
429
|
-
return {
|
|
430
|
-
kind: "tagMatch",
|
|
431
|
-
scrutinee: matched.scrutinee,
|
|
432
|
-
typeName: matched.typeName,
|
|
433
|
-
cases: [{ variant: matched.variant, body: walkExpr(e.right) }],
|
|
434
|
-
fallthrough: { kind: "bool", value: true, ty: { kind: "bool" } },
|
|
435
|
-
ty: { kind: "bool" },
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
/** Rule (expression): `Array.isArray(x) ? a : b` — ternary narrowing for
|
|
439
|
-
* synth array-unions. Mirrors `ruleImplArrayIsArray` but at the conditional
|
|
440
|
-
* position rather than the `==>` position.
|
|
441
|
-
* → `tagMatch x { ArrayBranch => walkExpr(a) } fallthrough walkExpr(b)`
|
|
442
|
-
* (or NonArrayBranch when the condition is negated).
|
|
443
|
-
* Inside the matched arm, bare references to `x` are rewritten to the
|
|
444
|
-
* variant's payload field (e.g. `x.arr`) by `transformExpr` when emitting
|
|
445
|
-
* the tagMatch — same mechanism `ruleImplArrayIsArray` already relies on. */
|
|
446
|
-
function ruleConditionalArrayIsArray(e) {
|
|
447
|
-
if (e.kind !== "conditional")
|
|
448
|
-
return null;
|
|
449
|
-
const pos = parseArrayIsArrayCall(e.cond);
|
|
450
|
-
// `typeof x === "string"` is a positive check like `Array.isArray`, but selects
|
|
451
|
-
// the NonArrayBranch — its then-branch is the matched-variant body.
|
|
452
|
-
const tof = pos ? null : parseTypeofStringCheck(e.cond);
|
|
453
|
-
const neg = !pos && !tof && e.cond.kind === "unop" && e.cond.op === "!"
|
|
454
|
-
? parseArrayIsArrayCall(e.cond.expr)
|
|
455
|
-
: null;
|
|
456
|
-
const matched = pos ?? tof ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
|
|
457
|
-
if (!matched)
|
|
458
|
-
return null;
|
|
459
|
-
const positive = pos ?? tof;
|
|
460
|
-
const thenBody = positive ? e.then : e.else;
|
|
461
|
-
const elseBody = positive ? e.else : e.then;
|
|
462
|
-
return {
|
|
463
|
-
kind: "tagMatch",
|
|
464
|
-
scrutinee: matched.scrutinee,
|
|
465
|
-
typeName: matched.typeName,
|
|
466
|
-
cases: [{ variant: matched.variant, body: walkExpr(thenBody) }],
|
|
467
|
-
fallthrough: walkExpr(elseBody),
|
|
468
|
-
ty: e.ty,
|
|
469
|
-
};
|
|
287
|
+
return presentMatchExpr(check, someBody, noneBody, e.ty);
|
|
470
288
|
}
|
|
471
|
-
/**
|
|
472
|
-
*
|
|
473
|
-
* bind narrowed values that the conclusion can use.
|
|
289
|
+
/** Driver (expression): `(path !== undefined [&& rest]) ==> B` — presence
|
|
290
|
+
* fact in implication position (spec premises). The premise's optional
|
|
291
|
+
* checks bind narrowed values that the conclusion can use.
|
|
474
292
|
* → `someMatch path { Some(_p_val) => (rest ==> B), None => true }`.
|
|
475
|
-
* Walks the inner ==> recursively so chained checks
|
|
476
|
-
|
|
293
|
+
* Walks the inner ==> recursively so chained checks become nested
|
|
294
|
+
* someMatches. Spec form: no falsy gate (historical shape — presence
|
|
295
|
+
* suffices in the premise). */
|
|
296
|
+
function ruleImplOptional(e, ctx) {
|
|
477
297
|
if (e.kind !== "binop" || e.op !== "==>")
|
|
478
298
|
return null;
|
|
479
299
|
let check;
|
|
480
300
|
let restCond = null;
|
|
481
|
-
const extracted =
|
|
301
|
+
const extracted = leadingPresent(e.left);
|
|
482
302
|
if (extracted) {
|
|
483
303
|
check = extracted.check;
|
|
484
304
|
restCond = extracted.restCond;
|
|
485
305
|
}
|
|
486
306
|
else {
|
|
487
|
-
const c =
|
|
307
|
+
const c = presentFact(e.left);
|
|
488
308
|
if (!c || c.negated)
|
|
489
309
|
return null;
|
|
490
310
|
check = c;
|
|
@@ -495,42 +315,23 @@ function ruleImplOptional(e) {
|
|
|
495
315
|
return {
|
|
496
316
|
kind: "someMatch",
|
|
497
317
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
498
|
-
binder: check.
|
|
499
|
-
someBody: walkExpr(innerBody),
|
|
318
|
+
binder: check.binder,
|
|
319
|
+
someBody: walkExpr(innerBody, ctx),
|
|
500
320
|
noneBody: { kind: "bool", value: true, ty: { kind: "bool" } },
|
|
501
321
|
ty: { kind: "bool" },
|
|
502
322
|
};
|
|
503
323
|
}
|
|
504
|
-
/**
|
|
505
|
-
* shared by `ruleOptChain` (base = binder) and `ruleOptChainIndex` (base = arr[i]). */
|
|
506
|
-
function applyChain(body, chain) {
|
|
507
|
-
for (const step of chain) {
|
|
508
|
-
if (step.kind === "field")
|
|
509
|
-
body = { kind: "field", obj: body, field: step.name, ty: step.ty };
|
|
510
|
-
else if (step.kind === "index")
|
|
511
|
-
body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
|
|
512
|
-
else
|
|
513
|
-
body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
|
|
514
|
-
}
|
|
515
|
-
return body;
|
|
516
|
-
}
|
|
517
|
-
/** `0 <= idx && idx < arr.length` — the in-bounds guard for an array index. */
|
|
518
|
-
function arrayBoundsCond(arr, idx) {
|
|
519
|
-
const len = { kind: "field", obj: arr, field: "length", ty: { kind: "int" } };
|
|
520
|
-
const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
|
|
521
|
-
const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
|
|
522
|
-
return { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
|
|
523
|
-
}
|
|
524
|
-
/** Rule (expression): `left ?? right` — nullish coalescing.
|
|
324
|
+
/** Driver (expression): `left ?? right` — nullish coalescing.
|
|
525
325
|
* → `someMatch left { Some(_v) => _v, None => right }`.
|
|
526
|
-
* Single-evaluation: scrutinee may be any expression.
|
|
527
|
-
|
|
326
|
+
* Single-evaluation: scrutinee may be any expression. Presence-only
|
|
327
|
+
* semantics (`??` tests null/undefined, not falsiness) — no falsy gate. */
|
|
328
|
+
function ruleNullish(e, ctx) {
|
|
528
329
|
if (e.kind !== "nullish")
|
|
529
330
|
return null;
|
|
530
331
|
if (e.left.ty.kind !== "optional")
|
|
531
332
|
return null;
|
|
532
333
|
const innerTy = e.left.ty.inner;
|
|
533
|
-
const binder =
|
|
334
|
+
const binder = freshOcBinder(ctx);
|
|
534
335
|
return {
|
|
535
336
|
kind: "someMatch",
|
|
536
337
|
scrutinee: e.left, binder, binderTy: innerTy,
|
|
@@ -539,12 +340,12 @@ function ruleNullish(e) {
|
|
|
539
340
|
ty: e.ty,
|
|
540
341
|
};
|
|
541
342
|
}
|
|
542
|
-
/**
|
|
543
|
-
* Under noUncheckedIndexedAccess `arr[i]` is `T | undefined`,
|
|
544
|
-
* when out of bounds, so
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
343
|
+
/** Driver (expression): `arr[i] ?? right` — in-bounds fact in nullish
|
|
344
|
+
* position. Under noUncheckedIndexedAccess `arr[i]` is `T | undefined`,
|
|
345
|
+
* undefined exactly when out of bounds, so
|
|
346
|
+
* → `(0 <= i && i < arr.length) ? arr[i] : right`. The guarded `then`
|
|
347
|
+
* keeps the seq index in bounds for the backend. (Map index is already
|
|
348
|
+
* optional-typed and handled by ruleNullish above.) */
|
|
548
349
|
function ruleNullishIndex(e) {
|
|
549
350
|
if (e.kind !== "nullish")
|
|
550
351
|
return null;
|
|
@@ -555,14 +356,12 @@ function ruleNullishIndex(e) {
|
|
|
555
356
|
const cond = arrayBoundsCond(e.left.obj, e.left.idx);
|
|
556
357
|
return { kind: "conditional", cond, then: e.left, else: e.right, ty: e.ty };
|
|
557
358
|
}
|
|
558
|
-
/**
|
|
559
|
-
* the optChain sibling of ruleNullishIndex.
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
* (ruleOptChain itself bails here: an array index is typed as the non-optional
|
|
565
|
-
* element type, so its `?.` never reaches that rule.) */
|
|
359
|
+
/** Driver (expression): `arr[i]?.<chain>` — in-bounds fact in optChain
|
|
360
|
+
* position, the optChain sibling of ruleNullishIndex.
|
|
361
|
+
* → `(0 <= i && i < arr.length) ? <chain on arr[i]> : undefined`. The
|
|
362
|
+
* conditional's optional type makes transform wrap the in-bounds chain
|
|
363
|
+
* result in Some and the OOB branch in None. (ruleOptChain itself bails
|
|
364
|
+
* here: an array index is typed as the non-optional element type.) */
|
|
566
365
|
function ruleOptChainIndex(e) {
|
|
567
366
|
if (e.kind !== "optChain")
|
|
568
367
|
return null;
|
|
@@ -575,15 +374,13 @@ function ruleOptChainIndex(e) {
|
|
|
575
374
|
const undef = { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
576
375
|
return { kind: "conditional", cond, then: body, else: undef, ty: e.ty };
|
|
577
376
|
}
|
|
578
|
-
/**
|
|
579
|
-
* (`e: T | undefined`) but whose array-index initializer is total (`T`)
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
* well-typed; an in-bounds proof
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
* usual source is `noUncheckedIndexedAccess`, but the flag itself is never
|
|
586
|
-
* checked). Skipped when the element type is already optional: no mismatch. */
|
|
377
|
+
/** Driver (statement): reconcile a `const e = arr[i]` whose binding is optional
|
|
378
|
+
* (`e: T | undefined`) but whose array-index initializer is total (`T`) —
|
|
379
|
+
* in-bounds fact in initializer position. Model the index as its JS
|
|
380
|
+
* semantics — `e := inBounds ? arr[i] : undefined` — so `e` is a real
|
|
381
|
+
* `Option<T>` and a later `e?.f` someMatch is well-typed; an in-bounds proof
|
|
382
|
+
* makes the None branch dead. Skipped when the element type is already
|
|
383
|
+
* optional: no mismatch. */
|
|
587
384
|
function ruleOptionalIndexBinding(s) {
|
|
588
385
|
if (s.kind !== "let")
|
|
589
386
|
return null;
|
|
@@ -601,17 +398,17 @@ function ruleOptionalIndexBinding(s) {
|
|
|
601
398
|
const guarded = { kind: "conditional", cond, then: init, else: undef, ty: s.ty };
|
|
602
399
|
return { ...s, init: guarded };
|
|
603
400
|
}
|
|
604
|
-
/**
|
|
401
|
+
/** Driver (expression): `obj?.<chain>` — single-eval optional chain.
|
|
605
402
|
* → `someMatch obj { Some(_oc{N}_val) => apply(chain, _oc{N}_val), None => undefined }`.
|
|
606
403
|
* The someBody applies the chain to the binder directly (field/call/index),
|
|
607
404
|
* so transform doesn't substitute. Scrutinee can be any expression. */
|
|
608
|
-
function ruleOptChain(e) {
|
|
405
|
+
function ruleOptChain(e, ctx) {
|
|
609
406
|
if (e.kind !== "optChain")
|
|
610
407
|
return null;
|
|
611
408
|
if (e.obj.ty.kind !== "optional")
|
|
612
409
|
return null;
|
|
613
410
|
const innerTy = e.obj.ty.inner;
|
|
614
|
-
const binder =
|
|
411
|
+
const binder = freshOcBinder(ctx);
|
|
615
412
|
const body = applyChain({ kind: "var", name: binder, ty: innerTy }, e.chain);
|
|
616
413
|
const noneBody = { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
617
414
|
return {
|
|
@@ -620,41 +417,12 @@ function ruleOptChain(e) {
|
|
|
620
417
|
someBody: body, noneBody, ty: e.ty,
|
|
621
418
|
};
|
|
622
419
|
}
|
|
623
|
-
/**
|
|
624
|
-
*
|
|
625
|
-
* of a `k in m ? m[k] : default` ternary. */
|
|
626
|
-
function exprEqual(a, b) {
|
|
627
|
-
if (a.kind !== b.kind)
|
|
628
|
-
return false;
|
|
629
|
-
if (a.kind === "var" && b.kind === "var")
|
|
630
|
-
return a.name === b.name;
|
|
631
|
-
if (a.kind === "field" && b.kind === "field")
|
|
632
|
-
return a.field === b.field && exprEqual(a.obj, b.obj);
|
|
633
|
-
if (a.kind === "index" && b.kind === "index")
|
|
634
|
-
return exprEqual(a.obj, b.obj) && exprEqual(a.idx, b.idx);
|
|
635
|
-
return false;
|
|
636
|
-
}
|
|
637
|
-
/** Produce a reader-friendly binder hint for `m[k]` when both m and k are
|
|
638
|
-
* access-path shaped (var / field chain). Falls back to a generic counter
|
|
639
|
-
* name for computed keys. */
|
|
640
|
-
function binderHintForMapAccess(m, k) {
|
|
641
|
-
const mHint = binderHintFor(m);
|
|
642
|
-
const kHint = binderHintFor(k);
|
|
643
|
-
if (mHint && kHint) {
|
|
644
|
-
// mHint is `_m_val`, kHint is `_k_val` — stitch into `_m_k_val`.
|
|
645
|
-
const mStem = mHint.replace(/_val$/, "");
|
|
646
|
-
const kStem = kHint.replace(/^_/, "").replace(/_val$/, "");
|
|
647
|
-
return freshName(`${mStem}_${kStem}_val`);
|
|
648
|
-
}
|
|
649
|
-
return freshName(`_oc${_ocCounter++}_val`);
|
|
650
|
-
}
|
|
651
|
-
/** Rule (expression): `k in m ? m[k] : default` where m is map-typed.
|
|
652
|
-
* The then-branch must be exactly `m[k]` (same obj, same key). This mirrors
|
|
653
|
-
* the discriminant-`in` path (line 438) but gated on `map` instead of `user`.
|
|
420
|
+
/** Driver (expression): `k in m ? m[k] : default` — key-membership fact in
|
|
421
|
+
* ternary position, m map-typed. The then-branch must be exactly `m[k]`.
|
|
654
422
|
* → `someMatch m[k] { Some(_m_k_val) => _m_k_val, None => default }`.
|
|
655
423
|
* The existing Dafny peephole collapses the result to
|
|
656
424
|
* `if k in m then m[k] else default`. */
|
|
657
|
-
function ruleConditionalInMap(e) {
|
|
425
|
+
function ruleConditionalInMap(e, ctx) {
|
|
658
426
|
if (e.kind !== "conditional")
|
|
659
427
|
return null;
|
|
660
428
|
if (e.cond.kind !== "binop" || e.cond.op !== "in")
|
|
@@ -678,7 +446,7 @@ function ruleConditionalInMap(e) {
|
|
|
678
446
|
if (e.then.ty.kind !== "optional")
|
|
679
447
|
return null;
|
|
680
448
|
const innerTy = m.ty.value;
|
|
681
|
-
const binder = binderHintForMapAccess(m, k);
|
|
449
|
+
const binder = binderHintForMapAccess(m, k, ctx);
|
|
682
450
|
return {
|
|
683
451
|
kind: "someMatch",
|
|
684
452
|
scrutinee: e.then, binder, binderTy: innerTy,
|
|
@@ -686,8 +454,9 @@ function ruleConditionalInMap(e) {
|
|
|
686
454
|
noneBody: e.else, ty: innerTy,
|
|
687
455
|
};
|
|
688
456
|
}
|
|
689
|
-
/**
|
|
690
|
-
* Only fires for simple var or simple `obj.field` cond.
|
|
457
|
+
/** Driver (expression): `opt ? a : b` (truthiness — cond itself is optional).
|
|
458
|
+
* Only fires for simple var or simple `obj.field` cond. Historical shape:
|
|
459
|
+
* no falsy gate on the bound value (unlike the other truthiness positions). */
|
|
691
460
|
function ruleConditionalOptionalTruthy(e) {
|
|
692
461
|
if (e.kind !== "conditional")
|
|
693
462
|
return null;
|
|
@@ -704,293 +473,45 @@ function ruleConditionalOptionalTruthy(e) {
|
|
|
704
473
|
someBody: e.then, noneBody: e.else, ty: e.ty,
|
|
705
474
|
};
|
|
706
475
|
}
|
|
707
|
-
/**
|
|
708
|
-
*
|
|
709
|
-
* semantic weight, so either side is fine. Shared by the optional and
|
|
710
|
-
* Array.isArray chain extractors — they differ only in `parse`.
|
|
711
|
-
* `(x !== undefined && b) && c` → { check, restCond: b && c }. */
|
|
712
|
-
function extractLeftmostCheck(cond, parse) {
|
|
713
|
-
if (cond.kind !== "binop" || cond.op !== "&&")
|
|
714
|
-
return null;
|
|
715
|
-
const left = parse(cond.left);
|
|
716
|
-
if (left)
|
|
717
|
-
return { check: left, restCond: cond.right };
|
|
718
|
-
const right = parse(cond.right);
|
|
719
|
-
if (right)
|
|
720
|
-
return { check: right, restCond: cond.left };
|
|
721
|
-
if (cond.left.kind === "binop" && cond.left.op === "&&") {
|
|
722
|
-
const inner = extractLeftmostCheck(cond.left, parse);
|
|
723
|
-
if (inner)
|
|
724
|
-
return { check: inner.check, restCond: { ...cond, left: inner.restCond } };
|
|
725
|
-
}
|
|
726
|
-
if (cond.right.kind === "binop" && cond.right.op === "&&") {
|
|
727
|
-
const inner = extractLeftmostCheck(cond.right, parse);
|
|
728
|
-
if (inner)
|
|
729
|
-
return { check: inner.check, restCond: { ...cond, right: inner.restCond } };
|
|
730
|
-
}
|
|
731
|
-
return null;
|
|
732
|
-
}
|
|
733
|
-
/** `&&`-chain extractor for a positive optional check. */
|
|
734
|
-
function extractLeftmostOptionalCheck(cond) {
|
|
735
|
-
return extractLeftmostCheck(cond, e => {
|
|
736
|
-
const c = parseSimpleOptionalCheck(e);
|
|
737
|
-
return c && !c.negated ? c : null;
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
/** Rule: `if (x !== undefined && rest) then` (no else) where x is a pure
|
|
741
|
-
* access path.
|
|
476
|
+
/** Driver: `if (x !== undefined && rest) then` (no else) — presence fact in
|
|
477
|
+
* guarded-if position.
|
|
742
478
|
* → `someMatch x { Some(_x_val) => if rest then then; , None => {} }`.
|
|
743
|
-
* Walks the inner if back through narrow so that nested optional checks in
|
|
744
|
-
*
|
|
745
|
-
function ruleIfAndOptional(s) {
|
|
479
|
+
* Walks the inner if back through narrow so that nested optional checks in
|
|
480
|
+
* rest also become someMatches. */
|
|
481
|
+
function ruleIfAndOptional(s, ctx) {
|
|
746
482
|
if (s.kind !== "if")
|
|
747
483
|
return null;
|
|
748
484
|
if (s.else.length !== 0)
|
|
749
485
|
return null;
|
|
750
|
-
const extracted =
|
|
486
|
+
const extracted = leadingPresent(s.cond);
|
|
751
487
|
if (!extracted)
|
|
752
488
|
return null;
|
|
753
489
|
const { check, restCond } = extracted;
|
|
754
490
|
const innerIf = { kind: "if", cond: restCond, then: s.then, else: [] };
|
|
755
|
-
|
|
756
|
-
? [{ kind: "if", cond: bound(check), then: [walkStmt(innerIf)], else: [] }]
|
|
757
|
-
: [walkStmt(innerIf)];
|
|
758
|
-
return {
|
|
759
|
-
kind: "someMatch",
|
|
760
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
761
|
-
binder: check.binderHint,
|
|
762
|
-
someBody,
|
|
763
|
-
noneBody: [],
|
|
764
|
-
};
|
|
491
|
+
return presentMatchStmts(check, [walkStmt(innerIf, ctx)], []);
|
|
765
492
|
}
|
|
766
|
-
/**
|
|
767
|
-
* guard idiom, TS-equivalent to `if (x !== undefined) rest;`)
|
|
768
|
-
* pure access path.
|
|
493
|
+
/** Driver: a bare expression statement `x !== undefined && rest` (the
|
|
494
|
+
* `if`-less guard idiom, TS-equivalent to `if (x !== undefined) rest;`).
|
|
769
495
|
* → `someMatch x { Some(_x_val) => rest;, None => {} }`.
|
|
770
|
-
* Runs `rest` for effect inside the narrowed scope.
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
|
|
775
|
-
* position, so transform never ANF-lifts it out of the arm (which would drop
|
|
776
|
-
* the guard and reference the un-narrowed optional). */
|
|
777
|
-
function ruleExprStmtAndOptional(s) {
|
|
496
|
+
* Runs `rest` for effect inside the narrowed scope. Unlike the ternary
|
|
497
|
+
* driver (`ruleConditionalAndOptional`), a method call in `rest` is fine
|
|
498
|
+
* here: a statement-level someMatch arm keeps it in statement position, so
|
|
499
|
+
* transform never ANF-lifts it out of the arm. */
|
|
500
|
+
function ruleExprStmtAndOptional(s, ctx) {
|
|
778
501
|
if (s.kind !== "expr")
|
|
779
502
|
return null;
|
|
780
503
|
if (s.expr.kind !== "binop" || s.expr.op !== "&&")
|
|
781
504
|
return null;
|
|
782
|
-
const extracted =
|
|
505
|
+
const extracted = leadingPresent(s.expr);
|
|
783
506
|
if (!extracted)
|
|
784
507
|
return null;
|
|
785
508
|
const { check, restCond } = extracted;
|
|
786
509
|
const innerStmt = { kind: "expr", expr: restCond };
|
|
787
|
-
|
|
788
|
-
? [{ kind: "if", cond: bound(check), then: [walkStmt(innerStmt)], else: [] }]
|
|
789
|
-
: [walkStmt(innerStmt)];
|
|
790
|
-
return {
|
|
791
|
-
kind: "someMatch",
|
|
792
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
793
|
-
binder: check.binderHint,
|
|
794
|
-
someBody,
|
|
795
|
-
noneBody: [],
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
// ── Discriminant narrowing ──────────────────────────────────
|
|
799
|
-
/** Detect `Array.isArray(<path>)` where `<path>` is a var or a chain of
|
|
800
|
-
* field accesses rooted at a var, and the path's type is a synthesized
|
|
801
|
-
* array-union (discriminant `"__isArray__"`). Returns the variant name to
|
|
802
|
-
* narrow to. The scrutinee is whatever path the user wrote — downstream
|
|
803
|
-
* transforms substitute it inside the matched arm. */
|
|
804
|
-
function parseArrayIsArrayCall(call) {
|
|
805
|
-
if (call.kind !== "call")
|
|
806
|
-
return null;
|
|
807
|
-
if (call.fn.kind !== "field" || call.fn.field !== "isArray")
|
|
808
|
-
return null;
|
|
809
|
-
if (call.fn.obj.kind !== "var" || call.fn.obj.name !== "Array")
|
|
810
|
-
return null;
|
|
811
|
-
if (call.args.length !== 1)
|
|
812
|
-
return null;
|
|
813
|
-
const arg = call.args[0];
|
|
814
|
-
if (!isNarrowablePath(arg) || arg.ty.kind !== "user")
|
|
815
|
-
return null;
|
|
816
|
-
const baseTyName = arg.ty.name.includes("<") ? arg.ty.name.slice(0, arg.ty.name.indexOf("<")) : arg.ty.name;
|
|
817
|
-
const decl = _typeDecls.find(d => d.name === baseTyName);
|
|
818
|
-
if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
|
|
819
|
-
return null;
|
|
820
|
-
return { scrutinee: arg, typeName: arg.ty.name, variant: "ArrayBranch" };
|
|
821
|
-
}
|
|
822
|
-
/** Detect `typeof <path> === "string"` where `<path>`'s type is a synth array-
|
|
823
|
-
* union (`U | T[]`) AND its `NonArrayBranch` payload `U` is itself `string`.
|
|
824
|
-
* The runtime `=== "string"` test matches that branch only when `U` is string —
|
|
825
|
-
* for any other non-array payload (`number | T[]`, …) it never holds, so we must
|
|
826
|
-
* NOT narrow. Returns the `NonArrayBranch` variant; the dual of `Array.isArray`. */
|
|
827
|
-
function parseTypeofStringCheck(e) {
|
|
828
|
-
if (e.kind !== "binop" || e.op !== "===")
|
|
829
|
-
return null;
|
|
830
|
-
const tof = e.left.kind === "unop" && e.left.op === "typeof" ? e.left.expr
|
|
831
|
-
: e.right.kind === "unop" && e.right.op === "typeof" ? e.right.expr : null;
|
|
832
|
-
const lit = e.left.kind === "str" ? e.left.value : e.right.kind === "str" ? e.right.value : null;
|
|
833
|
-
if (!tof || lit !== "string")
|
|
834
|
-
return null;
|
|
835
|
-
if (!isNarrowablePath(tof) || tof.ty.kind !== "user")
|
|
836
|
-
return null;
|
|
837
|
-
const baseTyName = tof.ty.name.includes("<") ? tof.ty.name.slice(0, tof.ty.name.indexOf("<")) : tof.ty.name;
|
|
838
|
-
const decl = _typeDecls.find(d => d.name === baseTyName);
|
|
839
|
-
if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
|
|
840
|
-
return null;
|
|
841
|
-
const valTy = decl.variants?.find(v => v.name === "NonArrayBranch")?.fields.find(f => f.name === "val")?.type;
|
|
842
|
-
if (valTy?.kind !== "string")
|
|
843
|
-
return null; // guard: the non-array branch must actually be `string`
|
|
844
|
-
return { scrutinee: tof, typeName: tof.ty.name, variant: "NonArrayBranch" };
|
|
845
|
-
}
|
|
846
|
-
/** A "narrowable path" is a var or a chain of field accesses rooted at a var
|
|
847
|
-
* — i.e., pure and structurally addressable, so transforms can substitute
|
|
848
|
-
* occurrences inside a matched arm without worrying about re-evaluation. */
|
|
849
|
-
function isNarrowablePath(e) {
|
|
850
|
-
if (e.kind === "var")
|
|
851
|
-
return true;
|
|
852
|
-
if (e.kind === "field")
|
|
853
|
-
return isNarrowablePath(e.obj);
|
|
854
|
-
return false;
|
|
855
|
-
}
|
|
856
|
-
/** `&&`-chain extractor for `Array.isArray(path)` (positive form only — a negated
|
|
857
|
-
* `!Array.isArray(...)` would narrow to the wrong variant for then-body consumers,
|
|
858
|
-
* so those are left to the untouched-conditional path). */
|
|
859
|
-
function extractLeftmostArrayIsArrayCheck(cond) {
|
|
860
|
-
return extractLeftmostCheck(cond, parseArrayIsArrayCall);
|
|
861
|
-
}
|
|
862
|
-
/** Detect `x.kind === "variant"`, `'key' in x`, or `Array.isArray(x)` (synth
|
|
863
|
-
* array-union) as a positive discriminant check. Returns the scrutinee var
|
|
864
|
-
* (with its type), type name, and variant. */
|
|
865
|
-
function parseDiscriminantCond(cond) {
|
|
866
|
-
// Pattern: x.discriminant === "variant"
|
|
867
|
-
if (cond.kind === "binop" && cond.op === "===" && cond.right.kind === "str" &&
|
|
868
|
-
cond.left.kind === "field" && cond.left.isDiscriminant &&
|
|
869
|
-
cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") {
|
|
870
|
-
return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
871
|
-
}
|
|
872
|
-
// Pattern: 'key' in x — narrows x to the unique variant containing `key`.
|
|
873
|
-
if (cond.kind === "binop" && cond.op === "in" &&
|
|
874
|
-
cond.left.kind === "str" && cond.right.kind === "var" &&
|
|
875
|
-
cond.right.ty.kind === "user") {
|
|
876
|
-
const key = cond.left.value;
|
|
877
|
-
const typeName = cond.right.ty.name;
|
|
878
|
-
const baseTyName = typeName.includes("<") ? typeName.slice(0, typeName.indexOf("<")) : typeName;
|
|
879
|
-
const decl = _typeDecls.find(d => d.name === baseTyName);
|
|
880
|
-
if (decl?.kind === "discriminated-union" && decl.variants) {
|
|
881
|
-
const matches = decl.variants.filter(v => v.fields.some(f => f.name === key));
|
|
882
|
-
if (matches.length === 1) {
|
|
883
|
-
return { scrutinee: cond.right, typeName, variant: matches[0].name };
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
// Pattern: Array.isArray(x) — narrows x to the ArrayBranch variant of a
|
|
888
|
-
// synthesized array-union (discriminant "__isArray__"). Statement-level
|
|
889
|
-
// discriminant chains (`if (Array.isArray(x)) {...} else if (...)`) still
|
|
890
|
-
// require a bare-var scrutinee since the existing var-name-keyed
|
|
891
|
-
// replacement machinery in transform.ts only handles that shape; path
|
|
892
|
-
// scrutinees (e.g. `m.content`) are handled exclusively by
|
|
893
|
-
// `ruleConditionalArrayIsArray` and the expression-form tagMatch path.
|
|
894
|
-
const arrCheck = parseArrayIsArrayCall(cond);
|
|
895
|
-
if (arrCheck && arrCheck.scrutinee.kind === "var") {
|
|
896
|
-
return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: arrCheck.variant };
|
|
897
|
-
}
|
|
898
|
-
return null;
|
|
899
|
-
}
|
|
900
|
-
/** Detect `x.kind !== "variant"` (negative discriminant check) or
|
|
901
|
-
* `!Array.isArray(x)` (synth array-union, narrows to NonArrayBranch). */
|
|
902
|
-
function parseNegativeDiscriminantCond(cond) {
|
|
903
|
-
if (cond.kind === "binop" && cond.op === "!==" && cond.right.kind === "str" &&
|
|
904
|
-
cond.left.kind === "field" && cond.left.isDiscriminant &&
|
|
905
|
-
cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") {
|
|
906
|
-
return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
907
|
-
}
|
|
908
|
-
// Pattern: !Array.isArray(x) — narrows x to the NonArrayBranch variant.
|
|
909
|
-
// Same var-scrutinee restriction as parseDiscriminantCond.
|
|
910
|
-
if (cond.kind === "unop" && cond.op === "!") {
|
|
911
|
-
const arrCheck = parseArrayIsArrayCall(cond.expr);
|
|
912
|
-
if (arrCheck && arrCheck.scrutinee.kind === "var") {
|
|
913
|
-
return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: "NonArrayBranch" };
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
function isTerminating(stmts) {
|
|
919
|
-
if (stmts.length === 0)
|
|
920
|
-
return false;
|
|
921
|
-
const last = stmts[stmts.length - 1];
|
|
922
|
-
return last.kind === "return" || last.kind === "throw" || last.kind === "break" || last.kind === "continue";
|
|
923
|
-
}
|
|
924
|
-
/** Rule (list-level): consecutive `if (x.kind === "v") ...` chain → tagMatch.
|
|
925
|
-
* Walks consecutive top-level ifs on the same discriminator var; the first
|
|
926
|
-
* one with an else-branch ends the chain (else becomes fallthrough; if-else-if
|
|
927
|
-
* flattens into more cases). Returns the tagMatch and how many stmts consumed. */
|
|
928
|
-
function ruleDiscriminantChain(stmts) {
|
|
929
|
-
if (stmts.length === 0 || stmts[0].kind !== "if")
|
|
930
|
-
return null;
|
|
931
|
-
const first = parseDiscriminantCond(stmts[0].cond);
|
|
932
|
-
if (!first)
|
|
933
|
-
return null;
|
|
934
|
-
const cases = [];
|
|
935
|
-
function collectElse(s) {
|
|
936
|
-
const p = parseDiscriminantCond(s.cond);
|
|
937
|
-
if (!p || p.scrutinee.name !== first.scrutinee.name)
|
|
938
|
-
return [s];
|
|
939
|
-
cases.push({ variant: p.variant, body: s.then });
|
|
940
|
-
if (s.else.length === 0)
|
|
941
|
-
return [];
|
|
942
|
-
if (s.else.length === 1 && s.else[0].kind === "if")
|
|
943
|
-
return collectElse(s.else[0]);
|
|
944
|
-
return s.else;
|
|
945
|
-
}
|
|
946
|
-
let consumed = 0;
|
|
947
|
-
for (let i = 0; i < stmts.length; i++) {
|
|
948
|
-
const s = stmts[i];
|
|
949
|
-
if (s.kind !== "if")
|
|
950
|
-
break;
|
|
951
|
-
const p = parseDiscriminantCond(s.cond);
|
|
952
|
-
if (!p || p.scrutinee.name !== first.scrutinee.name)
|
|
953
|
-
break;
|
|
954
|
-
cases.push({ variant: p.variant, body: s.then });
|
|
955
|
-
consumed = i + 1;
|
|
956
|
-
if (s.else.length > 0) {
|
|
957
|
-
const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
|
|
958
|
-
return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
|
|
959
|
-
cases, fallthrough: ft }, consumed };
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
if (cases.length === 0)
|
|
963
|
-
return null;
|
|
964
|
-
// If every case terminates, the trailing statements are the default arm
|
|
965
|
-
// (preserving the clean dispatch-as-expression shape). Otherwise the tail runs
|
|
966
|
-
// after the match for every variant, so leave it to the caller (empty default)
|
|
967
|
-
// rather than mis-routing it into the default arm only.
|
|
968
|
-
if (cases.every(c => isTerminating(c.body))) {
|
|
969
|
-
return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
|
|
970
|
-
cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
|
|
971
|
-
}
|
|
972
|
-
return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
|
|
973
|
-
cases, fallthrough: [] }, consumed };
|
|
510
|
+
return presentMatchStmts(check, [walkStmt(innerStmt, ctx)], []);
|
|
974
511
|
}
|
|
975
|
-
/**
|
|
976
|
-
*
|
|
977
|
-
|
|
978
|
-
if (stmts.length < 2)
|
|
979
|
-
return null;
|
|
980
|
-
const first = stmts[0];
|
|
981
|
-
if (first.kind !== "if" || first.else.length > 0)
|
|
982
|
-
return null;
|
|
983
|
-
if (!isTerminating(first.then))
|
|
984
|
-
return null;
|
|
985
|
-
const cond = parseNegativeDiscriminantCond(first.cond);
|
|
986
|
-
if (!cond)
|
|
987
|
-
return null;
|
|
988
|
-
return { stmt: { kind: "tagMatch", scrutinee: cond.scrutinee, typeName: cond.typeName,
|
|
989
|
-
cases: [{ variant: cond.variant, body: stmts.slice(1) }], fallthrough: first.then },
|
|
990
|
-
consumed: stmts.length };
|
|
991
|
-
}
|
|
992
|
-
/** Rule (statement): `let x = (e_opt && rest) ? a : b` where rest may contain
|
|
993
|
-
* method calls. → `var x: T := b; someMatch e_opt { Some(_v) => { if rest { x := a } } }`.
|
|
512
|
+
/** Driver (statement): `let x = (e_opt && rest) ? a : b` — presence fact in
|
|
513
|
+
* conditional-initializer position, where rest may contain method calls.
|
|
514
|
+
* → `var x: T := b; someMatch e_opt { Some(_v) => { if rest { x := a } } }`.
|
|
994
515
|
* Statement-level form is needed because Dafny doesn't allow method calls
|
|
995
516
|
* inside match expression arms. */
|
|
996
517
|
function ruleLetCondAndOptional(s) {
|
|
@@ -998,45 +519,32 @@ function ruleLetCondAndOptional(s) {
|
|
|
998
519
|
return null;
|
|
999
520
|
if (s.init.kind !== "conditional")
|
|
1000
521
|
return null;
|
|
1001
|
-
const extracted =
|
|
522
|
+
const extracted = leadingPresent(s.init.cond);
|
|
1002
523
|
if (!extracted)
|
|
1003
524
|
return null;
|
|
1004
525
|
const { check, restCond } = extracted;
|
|
1005
526
|
const assignIf = { kind: "if", cond: restCond,
|
|
1006
527
|
then: [{ kind: "assign", target: s.name, value: s.init.then }], else: [] };
|
|
1007
|
-
const someBody = canBeFalsy(check)
|
|
1008
|
-
? [{ kind: "if", cond: bound(check), then: [assignIf], else: [] }]
|
|
1009
|
-
: [assignIf];
|
|
1010
|
-
const sm = {
|
|
1011
|
-
kind: "someMatch",
|
|
1012
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
1013
|
-
binder: check.binderHint,
|
|
1014
|
-
someBody,
|
|
1015
|
-
noneBody: [],
|
|
1016
|
-
};
|
|
1017
528
|
return [
|
|
1018
529
|
{ kind: "let", name: s.name, ty: s.ty, mutable: true, init: s.init.else },
|
|
1019
|
-
|
|
530
|
+
presentMatchStmts(check, [assignIf], []),
|
|
1020
531
|
];
|
|
1021
532
|
}
|
|
1022
|
-
/** Built-in collection methods that lower to pure Dafny expressions
|
|
1023
|
-
* (`x in arr`, `x in m`, `x in s`, `|s|`, `s.Keys`, etc.) even though they
|
|
1024
|
-
* carry `callKind: "method"` from resolve. Safe inside match arms. */
|
|
1025
|
-
const PURE_BUILTIN_METHODS = new Set([
|
|
1026
|
-
"includes", "has", "size", "length", "keys", "values",
|
|
1027
|
-
]);
|
|
1028
533
|
/** Does this expression contain a method call that would be lifted to a
|
|
1029
534
|
* var binding outside its containing expression by transform? Such calls
|
|
1030
535
|
* are unsafe inside a match arm — the lifted binding would reference a
|
|
1031
|
-
* name only valid in the arm.
|
|
536
|
+
* name only valid in the arm. Builtins whose registry entry is `pure`
|
|
537
|
+
* (they lower to pure Dafny expressions: `x in arr`, `x in m`, `s.Keys`,
|
|
538
|
+
* …) are exempt even though they carry `callKind: "method"`. */
|
|
1032
539
|
function containsMethodCall(e) {
|
|
1033
540
|
if (e.kind === "call" && e.callKind === "method" &&
|
|
1034
|
-
!(e.
|
|
541
|
+
!(e.builtinId !== undefined && builtinSpec(e.builtinId).pure)) {
|
|
1035
542
|
return true;
|
|
1036
543
|
}
|
|
1037
544
|
switch (e.kind) {
|
|
1038
545
|
case "var":
|
|
1039
546
|
case "num":
|
|
547
|
+
case "bigint":
|
|
1040
548
|
case "str":
|
|
1041
549
|
case "bool":
|
|
1042
550
|
case "havoc":
|
|
@@ -1064,19 +572,18 @@ function containsMethodCall(e) {
|
|
|
1064
572
|
(e.fallthrough ? containsMethodCall(e.fallthrough) : false);
|
|
1065
573
|
}
|
|
1066
574
|
}
|
|
1067
|
-
/**
|
|
575
|
+
/** Driver (expression): `x !== undefined && rest ? a : b` — presence fact in
|
|
576
|
+
* guarded-ternary position.
|
|
1068
577
|
* → `someMatch x { Some(_x_val) => if rest then a else b, None => b }`.
|
|
1069
|
-
* Walks the inner conditional back through narrow so chained checks
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
|
|
1075
|
-
* to a mutable var first. */
|
|
1076
|
-
function ruleConditionalAndOptional(e) {
|
|
578
|
+
* Walks the inner conditional back through narrow so chained checks become
|
|
579
|
+
* nested someMatches. Does NOT fire if the guard `rest` contains method
|
|
580
|
+
* calls — transform lifts those out of the match arm, breaking the binder
|
|
581
|
+
* scope. The original transform's let-desugar handles those by lifting to
|
|
582
|
+
* a mutable var first. */
|
|
583
|
+
function ruleConditionalAndOptional(e, ctx) {
|
|
1077
584
|
if (e.kind !== "conditional")
|
|
1078
585
|
return null;
|
|
1079
|
-
const extracted =
|
|
586
|
+
const extracted = leadingPresent(e.cond);
|
|
1080
587
|
if (!extracted)
|
|
1081
588
|
return null;
|
|
1082
589
|
const { check, restCond } = extracted;
|
|
@@ -1086,48 +593,92 @@ function ruleConditionalAndOptional(e) {
|
|
|
1086
593
|
kind: "conditional",
|
|
1087
594
|
cond: restCond, then: e.then, else: e.else, ty: e.ty,
|
|
1088
595
|
};
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
596
|
+
return presentMatchExpr(check, walkExpr(innerCond, ctx), e.else, e.ty);
|
|
597
|
+
}
|
|
598
|
+
// ── Variant drivers (discriminants and synth array-unions) ──
|
|
599
|
+
/** Driver (expression): `Array.isArray(x) ==> B` or `!Array.isArray(x) ==> B` —
|
|
600
|
+
* isArray fact in implication position (spec premises).
|
|
601
|
+
* → `tagMatch x { ArrayBranch => walkExpr(B), _ => true }` (or NonArrayBranch).
|
|
602
|
+
* The other variant becomes a vacuous-true fallthrough. */
|
|
603
|
+
function ruleImplArrayIsArray(e, ctx) {
|
|
604
|
+
if (e.kind !== "binop" || e.op !== "==>")
|
|
605
|
+
return null;
|
|
606
|
+
const pos = isArrayFact(e.left, ctx);
|
|
607
|
+
const neg = e.left.kind === "unop" && e.left.op === "!"
|
|
608
|
+
? isArrayFact(e.left.expr, ctx)
|
|
609
|
+
: null;
|
|
610
|
+
const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
|
|
611
|
+
if (!matched)
|
|
612
|
+
return null;
|
|
1093
613
|
return {
|
|
1094
|
-
kind: "
|
|
1095
|
-
scrutinee:
|
|
1096
|
-
|
|
1097
|
-
|
|
614
|
+
kind: "tagMatch",
|
|
615
|
+
scrutinee: matched.scrutinee,
|
|
616
|
+
typeName: matched.typeName,
|
|
617
|
+
cases: [{ variant: matched.variant, body: walkExpr(e.right, ctx) }],
|
|
618
|
+
fallthrough: { kind: "bool", value: true, ty: { kind: "bool" } },
|
|
619
|
+
ty: { kind: "bool" },
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
/** Driver (expression): `Array.isArray(x) ? a : b` — isArray fact in ternary
|
|
623
|
+
* position (also `typeof x === "string"`, which selects the NonArrayBranch).
|
|
624
|
+
* → `tagMatch x { <variant> => walkExpr(then-side) } fallthrough walkExpr(else-side)`.
|
|
625
|
+
* Inside the matched arm, bare references to `x` are rewritten to the
|
|
626
|
+
* variant's payload field by `transformExpr` when emitting the tagMatch. */
|
|
627
|
+
function ruleConditionalArrayIsArray(e, ctx) {
|
|
628
|
+
if (e.kind !== "conditional")
|
|
629
|
+
return null;
|
|
630
|
+
const pos = isArrayFact(e.cond, ctx);
|
|
631
|
+
// `typeof x === "string"` is a positive check like `Array.isArray`, but selects
|
|
632
|
+
// the NonArrayBranch — its then-branch is the matched-variant body.
|
|
633
|
+
const tof = pos ? null : typeofStringFact(e.cond, ctx);
|
|
634
|
+
const neg = !pos && !tof && e.cond.kind === "unop" && e.cond.op === "!"
|
|
635
|
+
? isArrayFact(e.cond.expr, ctx)
|
|
636
|
+
: null;
|
|
637
|
+
const matched = pos ?? tof ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
|
|
638
|
+
if (!matched)
|
|
639
|
+
return null;
|
|
640
|
+
const positive = pos ?? tof;
|
|
641
|
+
const thenBody = positive ? e.then : e.else;
|
|
642
|
+
const elseBody = positive ? e.else : e.then;
|
|
643
|
+
return {
|
|
644
|
+
kind: "tagMatch",
|
|
645
|
+
scrutinee: matched.scrutinee,
|
|
646
|
+
typeName: matched.typeName,
|
|
647
|
+
cases: [{ variant: matched.variant, body: walkExpr(thenBody, ctx) }],
|
|
648
|
+
fallthrough: walkExpr(elseBody, ctx),
|
|
649
|
+
ty: e.ty,
|
|
1098
650
|
};
|
|
1099
651
|
}
|
|
1100
|
-
/**
|
|
652
|
+
/** Driver: `if (<rest> && Array.isArray(path) && <more>) then [else]` —
|
|
653
|
+
* isArray fact in guarded-if position.
|
|
1101
654
|
* → `tagMatch path { ArrayBranch => if (<rest && more>) then [else] }`.
|
|
1102
655
|
* The remaining conjuncts move inside the matched arm so any narrowing the
|
|
1103
|
-
* `then` body relies on (typed `path` accesses) sees the unwrapped variant.
|
|
1104
|
-
|
|
1105
|
-
function ruleIfAndArrayIsArray(s) {
|
|
656
|
+
* `then` body relies on (typed `path` accesses) sees the unwrapped variant. */
|
|
657
|
+
function ruleIfAndArrayIsArray(s, ctx) {
|
|
1106
658
|
if (s.kind !== "if")
|
|
1107
659
|
return null;
|
|
1108
|
-
const extracted =
|
|
660
|
+
const extracted = leadingIsArray(s.cond, ctx);
|
|
1109
661
|
if (!extracted)
|
|
1110
662
|
return null;
|
|
1111
663
|
const { check, restCond } = extracted;
|
|
1112
|
-
// Inner if uses the remaining conjunction
|
|
1113
|
-
//
|
|
1114
|
-
// conjunct). Walk recursively so nested checks compose.
|
|
664
|
+
// Inner if uses the remaining conjunction. Walk recursively so nested
|
|
665
|
+
// checks compose.
|
|
1115
666
|
const innerThen = [{ kind: "if", cond: restCond, then: s.then, else: s.else }];
|
|
1116
667
|
return {
|
|
1117
668
|
kind: "tagMatch",
|
|
1118
669
|
scrutinee: check.scrutinee,
|
|
1119
670
|
typeName: check.typeName,
|
|
1120
|
-
cases: [{ variant: check.variant, body: innerThen.map(walkStmt) }],
|
|
671
|
+
cases: [{ variant: check.variant, body: innerThen.map(x => walkStmt(x, ctx)) }],
|
|
1121
672
|
fallthrough: s.else,
|
|
1122
673
|
};
|
|
1123
674
|
}
|
|
1124
|
-
/**
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1127
|
-
function ruleConditionalAndArrayIsArray(e) {
|
|
675
|
+
/** Driver (expression): `(<rest> && Array.isArray(path)) ? a : b` — isArray
|
|
676
|
+
* fact in guarded-ternary position.
|
|
677
|
+
* → `tagMatch path { ArrayBranch => (<rest>) ? a : b } fallthrough b`. */
|
|
678
|
+
function ruleConditionalAndArrayIsArray(e, ctx) {
|
|
1128
679
|
if (e.kind !== "conditional")
|
|
1129
680
|
return null;
|
|
1130
|
-
const extracted =
|
|
681
|
+
const extracted = leadingIsArray(e.cond, ctx);
|
|
1131
682
|
if (!extracted)
|
|
1132
683
|
return null;
|
|
1133
684
|
const { check, restCond } = extracted;
|
|
@@ -1139,31 +690,99 @@ function ruleConditionalAndArrayIsArray(e) {
|
|
|
1139
690
|
kind: "tagMatch",
|
|
1140
691
|
scrutinee: check.scrutinee,
|
|
1141
692
|
typeName: check.typeName,
|
|
1142
|
-
cases: [{ variant: check.variant, body: walkExpr(innerCond) }],
|
|
693
|
+
cases: [{ variant: check.variant, body: walkExpr(innerCond, ctx) }],
|
|
1143
694
|
fallthrough: e.else,
|
|
1144
695
|
ty: e.ty,
|
|
1145
696
|
};
|
|
1146
697
|
}
|
|
698
|
+
/** Driver (list-level): consecutive `if (x.kind === "v") ...` chain →
|
|
699
|
+
* tagMatch. Walks consecutive top-level ifs on the same discriminator var;
|
|
700
|
+
* the first one with an else-branch ends the chain (else becomes
|
|
701
|
+
* fallthrough; if-else-if flattens into more cases). Returns the tagMatch
|
|
702
|
+
* and how many stmts consumed. */
|
|
703
|
+
function ruleDiscriminantChain(stmts, ctx) {
|
|
704
|
+
if (stmts.length === 0 || stmts[0].kind !== "if")
|
|
705
|
+
return null;
|
|
706
|
+
const first = variantFact(stmts[0].cond, ctx);
|
|
707
|
+
if (!first)
|
|
708
|
+
return null;
|
|
709
|
+
const cases = [];
|
|
710
|
+
function collectElse(s) {
|
|
711
|
+
const p = variantFact(s.cond, ctx);
|
|
712
|
+
if (!p || p.scrutinee.name !== first.scrutinee.name)
|
|
713
|
+
return [s];
|
|
714
|
+
cases.push({ variant: p.variant, body: s.then });
|
|
715
|
+
if (s.else.length === 0)
|
|
716
|
+
return [];
|
|
717
|
+
if (s.else.length === 1 && s.else[0].kind === "if")
|
|
718
|
+
return collectElse(s.else[0]);
|
|
719
|
+
return s.else;
|
|
720
|
+
}
|
|
721
|
+
let consumed = 0;
|
|
722
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
723
|
+
const s = stmts[i];
|
|
724
|
+
if (s.kind !== "if")
|
|
725
|
+
break;
|
|
726
|
+
const p = variantFact(s.cond, ctx);
|
|
727
|
+
if (!p || p.scrutinee.name !== first.scrutinee.name)
|
|
728
|
+
break;
|
|
729
|
+
cases.push({ variant: p.variant, body: s.then });
|
|
730
|
+
consumed = i + 1;
|
|
731
|
+
if (s.else.length > 0) {
|
|
732
|
+
const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
|
|
733
|
+
return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
|
|
734
|
+
cases, fallthrough: ft }, consumed };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (cases.length === 0)
|
|
738
|
+
return null;
|
|
739
|
+
// If every case terminates, the trailing statements are the default arm
|
|
740
|
+
// (preserving the clean dispatch-as-expression shape). Otherwise the tail runs
|
|
741
|
+
// after the match for every variant, so leave it to the caller (empty default)
|
|
742
|
+
// rather than mis-routing it into the default arm only.
|
|
743
|
+
if (cases.every(c => isTerminating(c.body))) {
|
|
744
|
+
return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
|
|
745
|
+
cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
|
|
746
|
+
}
|
|
747
|
+
return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
|
|
748
|
+
cases, fallthrough: [] }, consumed };
|
|
749
|
+
}
|
|
750
|
+
/** Driver (list-level): `if (x.kind !== "v") terminate; rest` → tagMatch
|
|
751
|
+
* with cases = [{ variant: v, body: rest }] and fallthrough = terminate. */
|
|
752
|
+
function ruleDiscriminantNegEarlyReturn(stmts, ctx) {
|
|
753
|
+
if (stmts.length < 2)
|
|
754
|
+
return null;
|
|
755
|
+
const first = stmts[0];
|
|
756
|
+
if (first.kind !== "if" || first.else.length > 0)
|
|
757
|
+
return null;
|
|
758
|
+
if (!isTerminating(first.then))
|
|
759
|
+
return null;
|
|
760
|
+
const cond = negVariantFact(first.cond, ctx);
|
|
761
|
+
if (!cond)
|
|
762
|
+
return null;
|
|
763
|
+
return { stmt: { kind: "tagMatch", scrutinee: cond.scrutinee, typeName: cond.typeName,
|
|
764
|
+
cases: [{ variant: cond.variant, body: stmts.slice(1) }], fallthrough: first.then },
|
|
765
|
+
consumed: stmts.length };
|
|
766
|
+
}
|
|
1147
767
|
// ── Function / module entry ──────────────────────────────────
|
|
1148
|
-
function narrowFunction(fn) {
|
|
768
|
+
function narrowFunction(fn, ctx) {
|
|
1149
769
|
return {
|
|
1150
770
|
...fn,
|
|
1151
|
-
requires: fn.requires.map(e => walkExpr(e)),
|
|
1152
|
-
ensures: fn.ensures.map(e => walkExpr(e)),
|
|
1153
|
-
decreases: fn.decreases ? walkExpr(fn.decreases) : null,
|
|
1154
|
-
body: walkStmts(fn.body),
|
|
771
|
+
requires: fn.requires.map(e => walkExpr(e, ctx)),
|
|
772
|
+
ensures: fn.ensures.map(e => walkExpr(e, ctx)),
|
|
773
|
+
decreases: fn.decreases ? walkExpr(fn.decreases, ctx) : null,
|
|
774
|
+
body: walkStmts(fn.body, ctx),
|
|
1155
775
|
};
|
|
1156
776
|
}
|
|
1157
777
|
export function narrowModule(mod) {
|
|
1158
|
-
|
|
1159
|
-
_typeDecls = mod.typeDecls;
|
|
778
|
+
const ctx = { decls: mod.typeDecls, oc: { n: 0 } };
|
|
1160
779
|
return {
|
|
1161
780
|
...mod,
|
|
1162
|
-
constants: mod.constants.map(c => ({ ...c, value: walkExpr(c.value) })),
|
|
1163
|
-
functions: mod.functions.map(narrowFunction),
|
|
781
|
+
constants: mod.constants.map(c => ({ ...c, value: walkExpr(c.value, ctx) })),
|
|
782
|
+
functions: mod.functions.map(fn => narrowFunction(fn, ctx)),
|
|
1164
783
|
classes: mod.classes.map(cls => ({
|
|
1165
784
|
...cls,
|
|
1166
|
-
methods: cls.methods.map(narrowFunction),
|
|
785
|
+
methods: cls.methods.map(m => narrowFunction(m, ctx)),
|
|
1167
786
|
})),
|
|
1168
787
|
};
|
|
1169
788
|
}
|