lemmascript 0.5.18 → 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 +296 -677
- package/tools/dist/peephole.js +12 -94
- package/tools/dist/rawir.js +15 -1
- package/tools/dist/resolve.js +182 -203
- package/tools/dist/specparser.js +21 -17
- package/tools/dist/transform.js +298 -108
- package/tools/dist/typedecls.js +59 -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,83 +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
|
*/
|
|
37
36
|
import { isTerminatorKind } from "./typedir.js";
|
|
38
37
|
import { freshName } from "./names.js";
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let _ocCounter = 0;
|
|
42
|
-
/** Type declarations for this module. Set in narrowModule, used by the
|
|
43
|
-
* discriminant-narrowing rules to resolve `'key' in x` to a variant. */
|
|
44
|
-
let _typeDecls = [];
|
|
45
|
-
/** Detect optional checks: `e !== undefined`, `e === undefined`, or `!e` for a
|
|
46
|
-
* pure-access-path optional-typed e. `!e` is equivalent to `=== undefined`.
|
|
47
|
-
* Following TS, only pure access paths narrow; complex scrutinees return null. */
|
|
48
|
-
function parseOptionalCheck(cond) {
|
|
49
|
-
// `!e` where e is optional — a truthiness form: false iff e is absent OR its
|
|
50
|
-
// inner value is itself falsy (so `Some(0)`/`Some("")` count as falsy too).
|
|
51
|
-
if (cond.kind === "unop" && cond.op === "!" && cond.expr.ty.kind === "optional") {
|
|
52
|
-
const e = cond.expr;
|
|
53
|
-
const innerTy = cond.expr.ty.inner;
|
|
54
|
-
const hint = binderHintFor(e);
|
|
55
|
-
if (hint === null)
|
|
56
|
-
return null;
|
|
57
|
-
return { scrutinee: e, innerTy, negated: true, binderHint: freshName(hint), truthiness: true };
|
|
58
|
-
}
|
|
59
|
-
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
|
|
60
|
-
// Bare optional truthiness: `if (e)` where e: T | undefined — true iff e is
|
|
61
|
-
// present AND its inner value is truthy.
|
|
62
|
-
if (cond.ty.kind === "optional") {
|
|
63
|
-
const hint = binderHintFor(cond);
|
|
64
|
-
if (hint === null)
|
|
65
|
-
return null;
|
|
66
|
-
return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: freshName(hint), truthiness: true };
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
// Explicit `e === undefined` / `e !== undefined` — a pure presence check,
|
|
71
|
-
// independent of the inner value (so NOT a truthiness form).
|
|
72
|
-
let e = null;
|
|
73
|
-
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
74
|
-
e = cond.left;
|
|
75
|
-
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
76
|
-
e = cond.right;
|
|
77
|
-
if (!e || e.ty.kind !== "optional")
|
|
78
|
-
return null;
|
|
79
|
-
const hint = binderHintFor(e);
|
|
80
|
-
if (hint === null)
|
|
81
|
-
return null;
|
|
82
|
-
return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: freshName(hint), truthiness: false };
|
|
83
|
-
}
|
|
84
|
-
function binderHintFor(e) {
|
|
85
|
-
// Pure access paths: var(x) or field(purePath, name).
|
|
86
|
-
// Walks down to the var root, collecting field names. Returns
|
|
87
|
-
// `_root_field1_field2_..._val` (or `_root_val` for a bare var).
|
|
88
|
-
const fields = [];
|
|
89
|
-
let cur = e;
|
|
90
|
-
while (cur.kind === "field") {
|
|
91
|
-
fields.unshift(cur.field);
|
|
92
|
-
cur = cur.obj;
|
|
93
|
-
}
|
|
94
|
-
if (cur.kind !== "var")
|
|
95
|
-
return null;
|
|
96
|
-
// \result is stored as the IR var name "\\result"; sanitize for a valid identifier.
|
|
97
|
-
const root = cur.name === "\\result" ? "result" : cur.name;
|
|
98
|
-
return fields.length === 0 ? `_${root}_val` : `_${root}_${fields.join("_")}_val`;
|
|
99
|
-
}
|
|
100
|
-
// Aliased for code that historically called the simpler check.
|
|
101
|
-
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";
|
|
102
40
|
// ── Walkers ──────────────────────────────────────────────────
|
|
103
|
-
function walkExpr(e) {
|
|
104
|
-
const r = recurseExpr(e);
|
|
105
|
-
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;
|
|
106
44
|
}
|
|
107
|
-
function recurseExpr(e) {
|
|
108
|
-
const re = walkExpr;
|
|
45
|
+
function recurseExpr(e, ctx) {
|
|
46
|
+
const re = (x) => walkExpr(x, ctx);
|
|
109
47
|
switch (e.kind) {
|
|
110
48
|
case "var":
|
|
111
49
|
case "num":
|
|
50
|
+
case "bigint":
|
|
112
51
|
case "str":
|
|
113
52
|
case "bool":
|
|
114
53
|
case "havoc":
|
|
@@ -121,7 +60,7 @@ function recurseExpr(e) {
|
|
|
121
60
|
case "record": return { ...e, spread: e.spread ? re(e.spread) : null,
|
|
122
61
|
fields: e.fields.map(f => ({ ...f, value: re(f.value) })) };
|
|
123
62
|
case "arrayLiteral": return { ...e, elems: e.elems.map(re) };
|
|
124
|
-
case "lambda": return { ...e, body: walkStmts(e.body) };
|
|
63
|
+
case "lambda": return { ...e, body: walkStmts(e.body, ctx) };
|
|
125
64
|
case "conditional": return { ...e, cond: re(e.cond), then: re(e.then), else: re(e.else) };
|
|
126
65
|
case "optChain": return { ...e, obj: re(e.obj),
|
|
127
66
|
chain: e.chain.map(s => s.kind === "call" ? { ...s, args: s.args.map(re) }
|
|
@@ -136,9 +75,9 @@ function recurseExpr(e) {
|
|
|
136
75
|
fallthrough: e.fallthrough ? re(e.fallthrough) : null };
|
|
137
76
|
}
|
|
138
77
|
}
|
|
139
|
-
function walkStmt(s) {
|
|
78
|
+
function walkStmt(s, ctx) {
|
|
140
79
|
// Recurse into children first, then try rules at this node.
|
|
141
|
-
const r = recurseStmt(s);
|
|
80
|
+
const r = recurseStmt(s, ctx);
|
|
142
81
|
// Optional narrowing fires before Array.isArray narrowing: in a chain like
|
|
143
82
|
// `next && Array.isArray(next.content)` the optional check must unwrap `next`
|
|
144
83
|
// *outside* the array match, since `next.content` is unreachable until then.
|
|
@@ -146,41 +85,41 @@ function walkStmt(s) {
|
|
|
146
85
|
// array rule fires; independent narrows commute, so the order is harmless.)
|
|
147
86
|
// && rules fire before the simple rule because they produce nested ifs whose
|
|
148
87
|
// inner shape doesn't match the simple rule directly.
|
|
149
|
-
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;
|
|
150
89
|
}
|
|
151
|
-
function walkStmts(stmts) {
|
|
90
|
+
function walkStmts(stmts, ctx) {
|
|
152
91
|
const result = [];
|
|
153
92
|
for (let i = 0; i < stmts.length; i++) {
|
|
154
93
|
const s = stmts[i];
|
|
155
94
|
const rest = stmts.slice(i + 1);
|
|
156
95
|
// Discriminant rules consume a prefix of stmts; remaining is processed normally.
|
|
157
|
-
const tagged = ruleDiscriminantChain(stmts.slice(i)) ?? ruleDiscriminantNegEarlyReturn(stmts.slice(i));
|
|
96
|
+
const tagged = ruleDiscriminantChain(stmts.slice(i), ctx) ?? ruleDiscriminantNegEarlyReturn(stmts.slice(i), ctx);
|
|
158
97
|
if (tagged) {
|
|
159
|
-
result.push(walkStmt(tagged.stmt));
|
|
98
|
+
result.push(walkStmt(tagged.stmt, ctx));
|
|
160
99
|
i += tagged.consumed - 1;
|
|
161
100
|
continue;
|
|
162
101
|
}
|
|
163
|
-
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);
|
|
164
103
|
if (consumed) {
|
|
165
|
-
result.push(walkStmt(consumed));
|
|
104
|
+
result.push(walkStmt(consumed, ctx));
|
|
166
105
|
return result;
|
|
167
106
|
}
|
|
168
107
|
// walkStmt first — narrow's expression rules may rewrite the let init from
|
|
169
108
|
// `conditional` to `someMatch`, in which case the let-cond desugar shouldn't fire.
|
|
170
|
-
const walked = walkStmt(s);
|
|
109
|
+
const walked = walkStmt(s, ctx);
|
|
171
110
|
const expanded = ruleLetCondAndOptional(walked);
|
|
172
111
|
if (expanded) {
|
|
173
112
|
for (const x of expanded)
|
|
174
|
-
result.push(walkStmt(x));
|
|
113
|
+
result.push(walkStmt(x, ctx));
|
|
175
114
|
continue;
|
|
176
115
|
}
|
|
177
116
|
result.push(walked);
|
|
178
117
|
}
|
|
179
118
|
return result;
|
|
180
119
|
}
|
|
181
|
-
function recurseStmt(s) {
|
|
182
|
-
const re = walkExpr;
|
|
183
|
-
const rs = walkStmts;
|
|
120
|
+
function recurseStmt(s, ctx) {
|
|
121
|
+
const re = (x) => walkExpr(x, ctx);
|
|
122
|
+
const rs = (x) => walkStmts(x, ctx);
|
|
184
123
|
switch (s.kind) {
|
|
185
124
|
case "let": return { ...s, init: re(s.init) };
|
|
186
125
|
case "assign": return { ...s, value: re(s.value) };
|
|
@@ -211,40 +150,36 @@ function recurseStmt(s) {
|
|
|
211
150
|
fallthrough: rs(s.fallthrough) };
|
|
212
151
|
}
|
|
213
152
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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. */
|
|
220
162
|
function ruleIfOptionalSimple(s) {
|
|
221
163
|
if (s.kind !== "if")
|
|
222
164
|
return null;
|
|
223
|
-
const check =
|
|
165
|
+
const check = presentFact(s.cond);
|
|
224
166
|
if (!check)
|
|
225
167
|
return null;
|
|
226
168
|
const someBody = check.negated ? s.else : s.then;
|
|
227
169
|
const noneBody = check.negated ? s.then : s.else;
|
|
228
170
|
if (someBody.length === 0)
|
|
229
171
|
return null;
|
|
230
|
-
return
|
|
231
|
-
kind: "someMatch",
|
|
232
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
233
|
-
binder: check.binderHint,
|
|
234
|
-
someBody: canBeFalsy(check) ? [{ kind: "if", cond: bound(check), then: someBody, else: noneBody }] : someBody,
|
|
235
|
-
noneBody,
|
|
236
|
-
};
|
|
172
|
+
return presentMatchStmts(check, someBody, noneBody);
|
|
237
173
|
}
|
|
238
|
-
/**
|
|
239
|
-
*
|
|
240
|
-
* Fires when the Some branch is empty
|
|
241
|
-
* 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. */
|
|
242
177
|
function ruleEarlyReturnConsume(s, rest) {
|
|
243
178
|
if (s.kind !== "if")
|
|
244
179
|
return null;
|
|
245
180
|
if (rest.length === 0)
|
|
246
181
|
return null;
|
|
247
|
-
const check =
|
|
182
|
+
const check = presentFact(s.cond);
|
|
248
183
|
if (!check)
|
|
249
184
|
return null;
|
|
250
185
|
const someBranch = check.negated ? s.else : s.then;
|
|
@@ -253,58 +188,15 @@ function ruleEarlyReturnConsume(s, rest) {
|
|
|
253
188
|
return null;
|
|
254
189
|
if (!isTerminating(noneBranch))
|
|
255
190
|
return null;
|
|
256
|
-
return
|
|
257
|
-
kind: "someMatch",
|
|
258
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
259
|
-
binder: check.binderHint,
|
|
260
|
-
someBody: canBeFalsy(check) ? [{ kind: "if", cond: bound(check), then: rest, else: noneBranch }] : rest,
|
|
261
|
-
noneBody: noneBranch,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
/** Flatten a nested `||` chain into its leaf conditions. */
|
|
265
|
-
function flattenOr(e) {
|
|
266
|
-
if (e.kind === "binop" && e.op === "||")
|
|
267
|
-
return [...flattenOr(e.left), ...flattenOr(e.right)];
|
|
268
|
-
return [e];
|
|
191
|
+
return presentMatchStmts(check, rest, noneBranch);
|
|
269
192
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return null;
|
|
278
|
-
const binder = freshName(hint);
|
|
279
|
-
const unwrapped = applyChain({ kind: "var", name: binder, ty: oc.obj.ty.inner }, oc.chain);
|
|
280
|
-
if (unwrapped.kind === "field" && unwrapped.obj.ty.kind === "user") {
|
|
281
|
-
const base = unwrapped.obj.ty.name.replace(/<.*/, "");
|
|
282
|
-
const decl = _typeDecls.find(d => d.name === base);
|
|
283
|
-
if (decl?.kind === "discriminated-union" && decl.discriminant === unwrapped.field)
|
|
284
|
-
unwrapped.isDiscriminant = true;
|
|
285
|
-
}
|
|
286
|
-
const lit = leaf.left === oc ? leaf.right : leaf.left;
|
|
287
|
-
return { scrutinee: oc.obj, innerTy: oc.obj.ty.inner, binder, residual: { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } } };
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
// `!x` / `x === undefined`.
|
|
291
|
-
const chk = parseOptionalCheck(leaf);
|
|
292
|
-
if (chk && chk.negated) {
|
|
293
|
-
const residual = canBeFalsy(chk)
|
|
294
|
-
? { kind: "unop", op: "!", expr: { kind: "var", name: chk.binderHint, ty: chk.innerTy }, ty: { kind: "bool" } }
|
|
295
|
-
: null;
|
|
296
|
-
return { scrutinee: chk.scrutinee, innerTy: chk.innerTy, binder: chk.binderHint, residual };
|
|
297
|
-
}
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
/** Rule: `if (D1 || … || Dn) terminate; rest`. Each `Di` that detects some optional
|
|
301
|
-
* `x` is None (`!x`, `x === undefined`, `x?.chain !== lit`) narrows that `x` to Some
|
|
302
|
-
* across `rest`; the rest — value guards reading a narrowed `x` directly, plus the
|
|
303
|
-
* detectors' Some-case residuals — become a trailing early-return. Sound: reaching
|
|
304
|
-
* `rest` means every disjunct was false, so every detected optional is present.
|
|
305
|
-
* Covers `if (!x || x.f !== v) continue` / `if (x?.t !== 'm' || x.g) break`.
|
|
306
|
-
* Closes the resolve.ts:602 TODO ("|| narrowing"). */
|
|
307
|
-
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) {
|
|
308
200
|
if (s.kind !== "if")
|
|
309
201
|
return null;
|
|
310
202
|
if (rest.length === 0)
|
|
@@ -320,7 +212,7 @@ function ruleEarlyReturnOrChain(s, rest) {
|
|
|
320
212
|
const residualLeaves = [];
|
|
321
213
|
const seen = new Set();
|
|
322
214
|
for (const leaf of leaves) {
|
|
323
|
-
const d =
|
|
215
|
+
const d = noneDetector(leaf, ctx);
|
|
324
216
|
if (!d) {
|
|
325
217
|
residualLeaves.push(leaf);
|
|
326
218
|
continue;
|
|
@@ -344,17 +236,15 @@ function ruleEarlyReturnOrChain(s, rest) {
|
|
|
344
236
|
}
|
|
345
237
|
return inner[0];
|
|
346
238
|
}
|
|
347
|
-
/**
|
|
348
|
-
* `opt?.chain` is `undefined` when `opt` is None, and
|
|
349
|
-
* true, so the None case takes the terminating branch —
|
|
350
|
-
* `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
|
|
351
243
|
* someMatch opt { Some(v) => [if (v.chain !== lit) terminate; rest]; None => terminate }
|
|
352
|
-
* narrowing `opt` to `v` across `rest`
|
|
353
|
-
*
|
|
354
|
-
* discriminant narrowing). Bound-optional companion to ruleEarlyReturnConsume,
|
|
355
|
-
* 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
|
|
356
246
|
* `!==` so the None case is guaranteed to terminate. */
|
|
357
|
-
function ruleEarlyReturnOptChainCompare(s, rest) {
|
|
247
|
+
function ruleEarlyReturnOptChainCompare(s, rest, ctx) {
|
|
358
248
|
if (s.kind !== "if")
|
|
359
249
|
return null;
|
|
360
250
|
if (rest.length === 0)
|
|
@@ -375,16 +265,7 @@ function ruleEarlyReturnOptChainCompare(s, rest) {
|
|
|
375
265
|
const binder = freshName(hint);
|
|
376
266
|
const binderVar = { kind: "var", name: binder, ty: innerTy };
|
|
377
267
|
const unwrapped = applyChain(binderVar, oc.chain);
|
|
378
|
-
|
|
379
|
-
// on a direct `x.disc`; restore it when the unwrapped access is the binder
|
|
380
|
-
// union's discriminant, so the inner guard feeds discriminant narrowing.
|
|
381
|
-
if (unwrapped.kind === "field" && unwrapped.obj.ty.kind === "user") {
|
|
382
|
-
const base = unwrapped.obj.ty.name.replace(/<.*/, "");
|
|
383
|
-
const decl = _typeDecls.find(d => d.name === base);
|
|
384
|
-
if (decl?.kind === "discriminated-union" && decl.discriminant === unwrapped.field) {
|
|
385
|
-
unwrapped.isDiscriminant = true;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
268
|
+
restoreDiscriminantFlag(unwrapped, ctx.decls);
|
|
388
269
|
const innerGuard = { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } };
|
|
389
270
|
// Keep `rest` as trailing statements (not an else branch) — `s.then` terminates,
|
|
390
271
|
// so `if (g) terminate; rest` ≡ `if (g) terminate else rest`, and the trailing
|
|
@@ -393,99 +274,37 @@ function ruleEarlyReturnOptChainCompare(s, rest) {
|
|
|
393
274
|
const someBody = [{ kind: "if", cond: innerGuard, then: s.then, else: [] }, ...rest];
|
|
394
275
|
return { kind: "someMatch", scrutinee: oc.obj, binder, binderTy: innerTy, someBody, noneBody: s.then };
|
|
395
276
|
}
|
|
396
|
-
/**
|
|
277
|
+
/** Driver (expression): `e !== undefined ? a : b` — presence fact in
|
|
278
|
+
* ternary position. */
|
|
397
279
|
function ruleConditionalOptionalSimple(e) {
|
|
398
280
|
if (e.kind !== "conditional")
|
|
399
281
|
return null;
|
|
400
|
-
const check =
|
|
282
|
+
const check = presentFact(e.cond);
|
|
401
283
|
if (!check)
|
|
402
284
|
return null;
|
|
403
285
|
const someBody = check.negated ? e.else : e.then;
|
|
404
286
|
const noneBody = check.negated ? e.then : e.else;
|
|
405
|
-
return
|
|
406
|
-
kind: "someMatch",
|
|
407
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
408
|
-
binder: check.binderHint,
|
|
409
|
-
someBody: canBeFalsy(check) ? { kind: "conditional", cond: bound(check), then: someBody, else: noneBody, ty: e.ty } : someBody,
|
|
410
|
-
noneBody,
|
|
411
|
-
ty: e.ty,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
/** Rule (expression): `Array.isArray(x) ==> B` or `!Array.isArray(x) ==> B` —
|
|
415
|
-
* premise narrowing for spec implications. Mirrors `ruleImplOptional` but for
|
|
416
|
-
* synth array-union discriminators.
|
|
417
|
-
* → `tagMatch x { ArrayBranch => walkExpr(B), _ => true }` (or NonArrayBranch).
|
|
418
|
-
* The other variant becomes a vacuous-true fallthrough (the implication is
|
|
419
|
-
* trivially satisfied when the premise is false). */
|
|
420
|
-
function ruleImplArrayIsArray(e) {
|
|
421
|
-
if (e.kind !== "binop" || e.op !== "==>")
|
|
422
|
-
return null;
|
|
423
|
-
const pos = parseArrayIsArrayCall(e.left);
|
|
424
|
-
const neg = e.left.kind === "unop" && e.left.op === "!"
|
|
425
|
-
? parseArrayIsArrayCall(e.left.expr)
|
|
426
|
-
: null;
|
|
427
|
-
const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
|
|
428
|
-
if (!matched)
|
|
429
|
-
return null;
|
|
430
|
-
return {
|
|
431
|
-
kind: "tagMatch",
|
|
432
|
-
scrutinee: matched.scrutinee,
|
|
433
|
-
typeName: matched.typeName,
|
|
434
|
-
cases: [{ variant: matched.variant, body: walkExpr(e.right) }],
|
|
435
|
-
fallthrough: { kind: "bool", value: true, ty: { kind: "bool" } },
|
|
436
|
-
ty: { kind: "bool" },
|
|
437
|
-
};
|
|
287
|
+
return presentMatchExpr(check, someBody, noneBody, e.ty);
|
|
438
288
|
}
|
|
439
|
-
/**
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
* → `tagMatch x { ArrayBranch => walkExpr(a) } fallthrough walkExpr(b)`
|
|
443
|
-
* (or NonArrayBranch when the condition is negated).
|
|
444
|
-
* Inside the matched arm, bare references to `x` are rewritten to the
|
|
445
|
-
* variant's payload field (e.g. `x.arr`) by `transformExpr` when emitting
|
|
446
|
-
* the tagMatch — same mechanism `ruleImplArrayIsArray` already relies on. */
|
|
447
|
-
function ruleConditionalArrayIsArray(e) {
|
|
448
|
-
if (e.kind !== "conditional")
|
|
449
|
-
return null;
|
|
450
|
-
const pos = parseArrayIsArrayCall(e.cond);
|
|
451
|
-
// `typeof x === "string"` is a positive check like `Array.isArray`, but selects
|
|
452
|
-
// the NonArrayBranch — its then-branch is the matched-variant body.
|
|
453
|
-
const tof = pos ? null : parseTypeofStringCheck(e.cond);
|
|
454
|
-
const neg = !pos && !tof && e.cond.kind === "unop" && e.cond.op === "!"
|
|
455
|
-
? parseArrayIsArrayCall(e.cond.expr)
|
|
456
|
-
: null;
|
|
457
|
-
const matched = pos ?? tof ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
|
|
458
|
-
if (!matched)
|
|
459
|
-
return null;
|
|
460
|
-
const positive = pos ?? tof;
|
|
461
|
-
const thenBody = positive ? e.then : e.else;
|
|
462
|
-
const elseBody = positive ? e.else : e.then;
|
|
463
|
-
return {
|
|
464
|
-
kind: "tagMatch",
|
|
465
|
-
scrutinee: matched.scrutinee,
|
|
466
|
-
typeName: matched.typeName,
|
|
467
|
-
cases: [{ variant: matched.variant, body: walkExpr(thenBody) }],
|
|
468
|
-
fallthrough: walkExpr(elseBody),
|
|
469
|
-
ty: e.ty,
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
/** Rule (expression): `(path !== undefined [&& rest]) ==> B` — premise narrowing
|
|
473
|
-
* for spec implications (ensures/requires). The premise's optional checks
|
|
474
|
-
* 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.
|
|
475
292
|
* → `someMatch path { Some(_p_val) => (rest ==> B), None => true }`.
|
|
476
|
-
* Walks the inner ==> recursively so chained checks
|
|
477
|
-
|
|
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) {
|
|
478
297
|
if (e.kind !== "binop" || e.op !== "==>")
|
|
479
298
|
return null;
|
|
480
299
|
let check;
|
|
481
300
|
let restCond = null;
|
|
482
|
-
const extracted =
|
|
301
|
+
const extracted = leadingPresent(e.left);
|
|
483
302
|
if (extracted) {
|
|
484
303
|
check = extracted.check;
|
|
485
304
|
restCond = extracted.restCond;
|
|
486
305
|
}
|
|
487
306
|
else {
|
|
488
|
-
const c =
|
|
307
|
+
const c = presentFact(e.left);
|
|
489
308
|
if (!c || c.negated)
|
|
490
309
|
return null;
|
|
491
310
|
check = c;
|
|
@@ -496,42 +315,23 @@ function ruleImplOptional(e) {
|
|
|
496
315
|
return {
|
|
497
316
|
kind: "someMatch",
|
|
498
317
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
499
|
-
binder: check.
|
|
500
|
-
someBody: walkExpr(innerBody),
|
|
318
|
+
binder: check.binder,
|
|
319
|
+
someBody: walkExpr(innerBody, ctx),
|
|
501
320
|
noneBody: { kind: "bool", value: true, ty: { kind: "bool" } },
|
|
502
321
|
ty: { kind: "bool" },
|
|
503
322
|
};
|
|
504
323
|
}
|
|
505
|
-
/**
|
|
506
|
-
* shared by `ruleOptChain` (base = binder) and `ruleOptChainIndex` (base = arr[i]). */
|
|
507
|
-
function applyChain(body, chain) {
|
|
508
|
-
for (const step of chain) {
|
|
509
|
-
if (step.kind === "field")
|
|
510
|
-
body = { kind: "field", obj: body, field: step.name, ty: step.ty };
|
|
511
|
-
else if (step.kind === "index")
|
|
512
|
-
body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
|
|
513
|
-
else
|
|
514
|
-
body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
|
|
515
|
-
}
|
|
516
|
-
return body;
|
|
517
|
-
}
|
|
518
|
-
/** `0 <= idx && idx < arr.length` — the in-bounds guard for an array index. */
|
|
519
|
-
function arrayBoundsCond(arr, idx) {
|
|
520
|
-
const len = { kind: "field", obj: arr, field: "length", ty: { kind: "int" } };
|
|
521
|
-
const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
|
|
522
|
-
const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
|
|
523
|
-
return { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
|
|
524
|
-
}
|
|
525
|
-
/** Rule (expression): `left ?? right` — nullish coalescing.
|
|
324
|
+
/** Driver (expression): `left ?? right` — nullish coalescing.
|
|
526
325
|
* → `someMatch left { Some(_v) => _v, None => right }`.
|
|
527
|
-
* Single-evaluation: scrutinee may be any expression.
|
|
528
|
-
|
|
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) {
|
|
529
329
|
if (e.kind !== "nullish")
|
|
530
330
|
return null;
|
|
531
331
|
if (e.left.ty.kind !== "optional")
|
|
532
332
|
return null;
|
|
533
333
|
const innerTy = e.left.ty.inner;
|
|
534
|
-
const binder =
|
|
334
|
+
const binder = freshOcBinder(ctx);
|
|
535
335
|
return {
|
|
536
336
|
kind: "someMatch",
|
|
537
337
|
scrutinee: e.left, binder, binderTy: innerTy,
|
|
@@ -540,12 +340,12 @@ function ruleNullish(e) {
|
|
|
540
340
|
ty: e.ty,
|
|
541
341
|
};
|
|
542
342
|
}
|
|
543
|
-
/**
|
|
544
|
-
* Under noUncheckedIndexedAccess `arr[i]` is `T | undefined`,
|
|
545
|
-
* when out of bounds, so
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
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.) */
|
|
549
349
|
function ruleNullishIndex(e) {
|
|
550
350
|
if (e.kind !== "nullish")
|
|
551
351
|
return null;
|
|
@@ -556,14 +356,12 @@ function ruleNullishIndex(e) {
|
|
|
556
356
|
const cond = arrayBoundsCond(e.left.obj, e.left.idx);
|
|
557
357
|
return { kind: "conditional", cond, then: e.left, else: e.right, ty: e.ty };
|
|
558
358
|
}
|
|
559
|
-
/**
|
|
560
|
-
* the optChain sibling of ruleNullishIndex.
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
* (ruleOptChain itself bails here: an array index is typed as the non-optional
|
|
566
|
-
* 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.) */
|
|
567
365
|
function ruleOptChainIndex(e) {
|
|
568
366
|
if (e.kind !== "optChain")
|
|
569
367
|
return null;
|
|
@@ -576,15 +374,13 @@ function ruleOptChainIndex(e) {
|
|
|
576
374
|
const undef = { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
577
375
|
return { kind: "conditional", cond, then: body, else: undef, ty: e.ty };
|
|
578
376
|
}
|
|
579
|
-
/**
|
|
580
|
-
* (`e: T | undefined`) but whose array-index initializer is total (`T`)
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
* well-typed; an in-bounds proof
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
* usual source is `noUncheckedIndexedAccess`, but the flag itself is never
|
|
587
|
-
* 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. */
|
|
588
384
|
function ruleOptionalIndexBinding(s) {
|
|
589
385
|
if (s.kind !== "let")
|
|
590
386
|
return null;
|
|
@@ -602,17 +398,17 @@ function ruleOptionalIndexBinding(s) {
|
|
|
602
398
|
const guarded = { kind: "conditional", cond, then: init, else: undef, ty: s.ty };
|
|
603
399
|
return { ...s, init: guarded };
|
|
604
400
|
}
|
|
605
|
-
/**
|
|
401
|
+
/** Driver (expression): `obj?.<chain>` — single-eval optional chain.
|
|
606
402
|
* → `someMatch obj { Some(_oc{N}_val) => apply(chain, _oc{N}_val), None => undefined }`.
|
|
607
403
|
* The someBody applies the chain to the binder directly (field/call/index),
|
|
608
404
|
* so transform doesn't substitute. Scrutinee can be any expression. */
|
|
609
|
-
function ruleOptChain(e) {
|
|
405
|
+
function ruleOptChain(e, ctx) {
|
|
610
406
|
if (e.kind !== "optChain")
|
|
611
407
|
return null;
|
|
612
408
|
if (e.obj.ty.kind !== "optional")
|
|
613
409
|
return null;
|
|
614
410
|
const innerTy = e.obj.ty.inner;
|
|
615
|
-
const binder =
|
|
411
|
+
const binder = freshOcBinder(ctx);
|
|
616
412
|
const body = applyChain({ kind: "var", name: binder, ty: innerTy }, e.chain);
|
|
617
413
|
const noneBody = { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
618
414
|
return {
|
|
@@ -621,41 +417,12 @@ function ruleOptChain(e) {
|
|
|
621
417
|
someBody: body, noneBody, ty: e.ty,
|
|
622
418
|
};
|
|
623
419
|
}
|
|
624
|
-
/**
|
|
625
|
-
*
|
|
626
|
-
* of a `k in m ? m[k] : default` ternary. */
|
|
627
|
-
function exprEqual(a, b) {
|
|
628
|
-
if (a.kind !== b.kind)
|
|
629
|
-
return false;
|
|
630
|
-
if (a.kind === "var" && b.kind === "var")
|
|
631
|
-
return a.name === b.name;
|
|
632
|
-
if (a.kind === "field" && b.kind === "field")
|
|
633
|
-
return a.field === b.field && exprEqual(a.obj, b.obj);
|
|
634
|
-
if (a.kind === "index" && b.kind === "index")
|
|
635
|
-
return exprEqual(a.obj, b.obj) && exprEqual(a.idx, b.idx);
|
|
636
|
-
return false;
|
|
637
|
-
}
|
|
638
|
-
/** Produce a reader-friendly binder hint for `m[k]` when both m and k are
|
|
639
|
-
* access-path shaped (var / field chain). Falls back to a generic counter
|
|
640
|
-
* name for computed keys. */
|
|
641
|
-
function binderHintForMapAccess(m, k) {
|
|
642
|
-
const mHint = binderHintFor(m);
|
|
643
|
-
const kHint = binderHintFor(k);
|
|
644
|
-
if (mHint && kHint) {
|
|
645
|
-
// mHint is `_m_val`, kHint is `_k_val` — stitch into `_m_k_val`.
|
|
646
|
-
const mStem = mHint.replace(/_val$/, "");
|
|
647
|
-
const kStem = kHint.replace(/^_/, "").replace(/_val$/, "");
|
|
648
|
-
return freshName(`${mStem}_${kStem}_val`);
|
|
649
|
-
}
|
|
650
|
-
return freshName(`_oc${_ocCounter++}_val`);
|
|
651
|
-
}
|
|
652
|
-
/** Rule (expression): `k in m ? m[k] : default` where m is map-typed.
|
|
653
|
-
* The then-branch must be exactly `m[k]` (same obj, same key). This mirrors
|
|
654
|
-
* 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]`.
|
|
655
422
|
* → `someMatch m[k] { Some(_m_k_val) => _m_k_val, None => default }`.
|
|
656
423
|
* The existing Dafny peephole collapses the result to
|
|
657
424
|
* `if k in m then m[k] else default`. */
|
|
658
|
-
function ruleConditionalInMap(e) {
|
|
425
|
+
function ruleConditionalInMap(e, ctx) {
|
|
659
426
|
if (e.kind !== "conditional")
|
|
660
427
|
return null;
|
|
661
428
|
if (e.cond.kind !== "binop" || e.cond.op !== "in")
|
|
@@ -679,7 +446,7 @@ function ruleConditionalInMap(e) {
|
|
|
679
446
|
if (e.then.ty.kind !== "optional")
|
|
680
447
|
return null;
|
|
681
448
|
const innerTy = m.ty.value;
|
|
682
|
-
const binder = binderHintForMapAccess(m, k);
|
|
449
|
+
const binder = binderHintForMapAccess(m, k, ctx);
|
|
683
450
|
return {
|
|
684
451
|
kind: "someMatch",
|
|
685
452
|
scrutinee: e.then, binder, binderTy: innerTy,
|
|
@@ -687,8 +454,9 @@ function ruleConditionalInMap(e) {
|
|
|
687
454
|
noneBody: e.else, ty: innerTy,
|
|
688
455
|
};
|
|
689
456
|
}
|
|
690
|
-
/**
|
|
691
|
-
* 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). */
|
|
692
460
|
function ruleConditionalOptionalTruthy(e) {
|
|
693
461
|
if (e.kind !== "conditional")
|
|
694
462
|
return null;
|
|
@@ -705,292 +473,45 @@ function ruleConditionalOptionalTruthy(e) {
|
|
|
705
473
|
someBody: e.then, noneBody: e.else, ty: e.ty,
|
|
706
474
|
};
|
|
707
475
|
}
|
|
708
|
-
/**
|
|
709
|
-
*
|
|
710
|
-
* semantic weight, so either side is fine. Shared by the optional and
|
|
711
|
-
* Array.isArray chain extractors — they differ only in `parse`.
|
|
712
|
-
* `(x !== undefined && b) && c` → { check, restCond: b && c }. */
|
|
713
|
-
function extractLeftmostCheck(cond, parse) {
|
|
714
|
-
if (cond.kind !== "binop" || cond.op !== "&&")
|
|
715
|
-
return null;
|
|
716
|
-
const left = parse(cond.left);
|
|
717
|
-
if (left)
|
|
718
|
-
return { check: left, restCond: cond.right };
|
|
719
|
-
const right = parse(cond.right);
|
|
720
|
-
if (right)
|
|
721
|
-
return { check: right, restCond: cond.left };
|
|
722
|
-
if (cond.left.kind === "binop" && cond.left.op === "&&") {
|
|
723
|
-
const inner = extractLeftmostCheck(cond.left, parse);
|
|
724
|
-
if (inner)
|
|
725
|
-
return { check: inner.check, restCond: { ...cond, left: inner.restCond } };
|
|
726
|
-
}
|
|
727
|
-
if (cond.right.kind === "binop" && cond.right.op === "&&") {
|
|
728
|
-
const inner = extractLeftmostCheck(cond.right, parse);
|
|
729
|
-
if (inner)
|
|
730
|
-
return { check: inner.check, restCond: { ...cond, right: inner.restCond } };
|
|
731
|
-
}
|
|
732
|
-
return null;
|
|
733
|
-
}
|
|
734
|
-
/** `&&`-chain extractor for a positive optional check. */
|
|
735
|
-
function extractLeftmostOptionalCheck(cond) {
|
|
736
|
-
return extractLeftmostCheck(cond, e => {
|
|
737
|
-
const c = parseSimpleOptionalCheck(e);
|
|
738
|
-
return c && !c.negated ? c : null;
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
/** Rule: `if (x !== undefined && rest) then` (no else) where x is a pure
|
|
742
|
-
* access path.
|
|
476
|
+
/** Driver: `if (x !== undefined && rest) then` (no else) — presence fact in
|
|
477
|
+
* guarded-if position.
|
|
743
478
|
* → `someMatch x { Some(_x_val) => if rest then then; , None => {} }`.
|
|
744
|
-
* Walks the inner if back through narrow so that nested optional checks in
|
|
745
|
-
*
|
|
746
|
-
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) {
|
|
747
482
|
if (s.kind !== "if")
|
|
748
483
|
return null;
|
|
749
484
|
if (s.else.length !== 0)
|
|
750
485
|
return null;
|
|
751
|
-
const extracted =
|
|
486
|
+
const extracted = leadingPresent(s.cond);
|
|
752
487
|
if (!extracted)
|
|
753
488
|
return null;
|
|
754
489
|
const { check, restCond } = extracted;
|
|
755
490
|
const innerIf = { kind: "if", cond: restCond, then: s.then, else: [] };
|
|
756
|
-
|
|
757
|
-
? [{ kind: "if", cond: bound(check), then: [walkStmt(innerIf)], else: [] }]
|
|
758
|
-
: [walkStmt(innerIf)];
|
|
759
|
-
return {
|
|
760
|
-
kind: "someMatch",
|
|
761
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
762
|
-
binder: check.binderHint,
|
|
763
|
-
someBody,
|
|
764
|
-
noneBody: [],
|
|
765
|
-
};
|
|
491
|
+
return presentMatchStmts(check, [walkStmt(innerIf, ctx)], []);
|
|
766
492
|
}
|
|
767
|
-
/**
|
|
768
|
-
* guard idiom, TS-equivalent to `if (x !== undefined) rest;`)
|
|
769
|
-
* 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;`).
|
|
770
495
|
* → `someMatch x { Some(_x_val) => rest;, None => {} }`.
|
|
771
|
-
* Runs `rest` for effect inside the narrowed scope.
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
|
|
776
|
-
* position, so transform never ANF-lifts it out of the arm (which would drop
|
|
777
|
-
* the guard and reference the un-narrowed optional). */
|
|
778
|
-
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) {
|
|
779
501
|
if (s.kind !== "expr")
|
|
780
502
|
return null;
|
|
781
503
|
if (s.expr.kind !== "binop" || s.expr.op !== "&&")
|
|
782
504
|
return null;
|
|
783
|
-
const extracted =
|
|
505
|
+
const extracted = leadingPresent(s.expr);
|
|
784
506
|
if (!extracted)
|
|
785
507
|
return null;
|
|
786
508
|
const { check, restCond } = extracted;
|
|
787
509
|
const innerStmt = { kind: "expr", expr: restCond };
|
|
788
|
-
|
|
789
|
-
? [{ kind: "if", cond: bound(check), then: [walkStmt(innerStmt)], else: [] }]
|
|
790
|
-
: [walkStmt(innerStmt)];
|
|
791
|
-
return {
|
|
792
|
-
kind: "someMatch",
|
|
793
|
-
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
794
|
-
binder: check.binderHint,
|
|
795
|
-
someBody,
|
|
796
|
-
noneBody: [],
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
// ── Discriminant narrowing ──────────────────────────────────
|
|
800
|
-
/** Detect `Array.isArray(<path>)` where `<path>` is a var or a chain of
|
|
801
|
-
* field accesses rooted at a var, and the path's type is a synthesized
|
|
802
|
-
* array-union (discriminant `"__isArray__"`). Returns the variant name to
|
|
803
|
-
* narrow to. The scrutinee is whatever path the user wrote — downstream
|
|
804
|
-
* transforms substitute it inside the matched arm. */
|
|
805
|
-
function parseArrayIsArrayCall(call) {
|
|
806
|
-
if (call.kind !== "call")
|
|
807
|
-
return null;
|
|
808
|
-
if (call.fn.kind !== "field" || call.fn.field !== "isArray")
|
|
809
|
-
return null;
|
|
810
|
-
if (call.fn.obj.kind !== "var" || call.fn.obj.name !== "Array")
|
|
811
|
-
return null;
|
|
812
|
-
if (call.args.length !== 1)
|
|
813
|
-
return null;
|
|
814
|
-
const arg = call.args[0];
|
|
815
|
-
if (!isNarrowablePath(arg) || arg.ty.kind !== "user")
|
|
816
|
-
return null;
|
|
817
|
-
const baseTyName = arg.ty.name.includes("<") ? arg.ty.name.slice(0, arg.ty.name.indexOf("<")) : arg.ty.name;
|
|
818
|
-
const decl = _typeDecls.find(d => d.name === baseTyName);
|
|
819
|
-
if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
|
|
820
|
-
return null;
|
|
821
|
-
return { scrutinee: arg, typeName: arg.ty.name, variant: "ArrayBranch" };
|
|
822
|
-
}
|
|
823
|
-
/** Detect `typeof <path> === "string"` where `<path>`'s type is a synth array-
|
|
824
|
-
* union (`U | T[]`) AND its `NonArrayBranch` payload `U` is itself `string`.
|
|
825
|
-
* The runtime `=== "string"` test matches that branch only when `U` is string —
|
|
826
|
-
* for any other non-array payload (`number | T[]`, …) it never holds, so we must
|
|
827
|
-
* NOT narrow. Returns the `NonArrayBranch` variant; the dual of `Array.isArray`. */
|
|
828
|
-
function parseTypeofStringCheck(e) {
|
|
829
|
-
if (e.kind !== "binop" || e.op !== "===")
|
|
830
|
-
return null;
|
|
831
|
-
const tof = e.left.kind === "unop" && e.left.op === "typeof" ? e.left.expr
|
|
832
|
-
: e.right.kind === "unop" && e.right.op === "typeof" ? e.right.expr : null;
|
|
833
|
-
const lit = e.left.kind === "str" ? e.left.value : e.right.kind === "str" ? e.right.value : null;
|
|
834
|
-
if (!tof || lit !== "string")
|
|
835
|
-
return null;
|
|
836
|
-
if (!isNarrowablePath(tof) || tof.ty.kind !== "user")
|
|
837
|
-
return null;
|
|
838
|
-
const baseTyName = tof.ty.name.includes("<") ? tof.ty.name.slice(0, tof.ty.name.indexOf("<")) : tof.ty.name;
|
|
839
|
-
const decl = _typeDecls.find(d => d.name === baseTyName);
|
|
840
|
-
if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
|
|
841
|
-
return null;
|
|
842
|
-
const valTy = decl.variants?.find(v => v.name === "NonArrayBranch")?.fields.find(f => f.name === "val")?.type;
|
|
843
|
-
if (valTy?.kind !== "string")
|
|
844
|
-
return null; // guard: the non-array branch must actually be `string`
|
|
845
|
-
return { scrutinee: tof, typeName: tof.ty.name, variant: "NonArrayBranch" };
|
|
846
|
-
}
|
|
847
|
-
/** A "narrowable path" is a var or a chain of field accesses rooted at a var
|
|
848
|
-
* — i.e., pure and structurally addressable, so transforms can substitute
|
|
849
|
-
* occurrences inside a matched arm without worrying about re-evaluation. */
|
|
850
|
-
function isNarrowablePath(e) {
|
|
851
|
-
if (e.kind === "var")
|
|
852
|
-
return true;
|
|
853
|
-
if (e.kind === "field")
|
|
854
|
-
return isNarrowablePath(e.obj);
|
|
855
|
-
return false;
|
|
856
|
-
}
|
|
857
|
-
/** `&&`-chain extractor for `Array.isArray(path)` (positive form only — a negated
|
|
858
|
-
* `!Array.isArray(...)` would narrow to the wrong variant for then-body consumers,
|
|
859
|
-
* so those are left to the untouched-conditional path). */
|
|
860
|
-
function extractLeftmostArrayIsArrayCheck(cond) {
|
|
861
|
-
return extractLeftmostCheck(cond, parseArrayIsArrayCall);
|
|
862
|
-
}
|
|
863
|
-
/** Detect `x.kind === "variant"`, `'key' in x`, or `Array.isArray(x)` (synth
|
|
864
|
-
* array-union) as a positive discriminant check. Returns the scrutinee var
|
|
865
|
-
* (with its type), type name, and variant. */
|
|
866
|
-
function parseDiscriminantCond(cond) {
|
|
867
|
-
// Pattern: x.discriminant === "variant"
|
|
868
|
-
if (cond.kind === "binop" && cond.op === "===" && cond.right.kind === "str" &&
|
|
869
|
-
cond.left.kind === "field" && cond.left.isDiscriminant &&
|
|
870
|
-
cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") {
|
|
871
|
-
return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
872
|
-
}
|
|
873
|
-
// Pattern: 'key' in x — narrows x to the unique variant containing `key`.
|
|
874
|
-
if (cond.kind === "binop" && cond.op === "in" &&
|
|
875
|
-
cond.left.kind === "str" && cond.right.kind === "var" &&
|
|
876
|
-
cond.right.ty.kind === "user") {
|
|
877
|
-
const key = cond.left.value;
|
|
878
|
-
const typeName = cond.right.ty.name;
|
|
879
|
-
const baseTyName = typeName.includes("<") ? typeName.slice(0, typeName.indexOf("<")) : typeName;
|
|
880
|
-
const decl = _typeDecls.find(d => d.name === baseTyName);
|
|
881
|
-
if (decl?.kind === "discriminated-union" && decl.variants) {
|
|
882
|
-
const matches = decl.variants.filter(v => v.fields.some(f => f.name === key));
|
|
883
|
-
if (matches.length === 1) {
|
|
884
|
-
return { scrutinee: cond.right, typeName, variant: matches[0].name };
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
// Pattern: Array.isArray(x) — narrows x to the ArrayBranch variant of a
|
|
889
|
-
// synthesized array-union (discriminant "__isArray__"). Statement-level
|
|
890
|
-
// discriminant chains (`if (Array.isArray(x)) {...} else if (...)`) still
|
|
891
|
-
// require a bare-var scrutinee since the existing var-name-keyed
|
|
892
|
-
// replacement machinery in transform.ts only handles that shape; path
|
|
893
|
-
// scrutinees (e.g. `m.content`) are handled exclusively by
|
|
894
|
-
// `ruleConditionalArrayIsArray` and the expression-form tagMatch path.
|
|
895
|
-
const arrCheck = parseArrayIsArrayCall(cond);
|
|
896
|
-
if (arrCheck && arrCheck.scrutinee.kind === "var") {
|
|
897
|
-
return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: arrCheck.variant };
|
|
898
|
-
}
|
|
899
|
-
return null;
|
|
900
|
-
}
|
|
901
|
-
/** Detect `x.kind !== "variant"` (negative discriminant check) or
|
|
902
|
-
* `!Array.isArray(x)` (synth array-union, narrows to NonArrayBranch). */
|
|
903
|
-
function parseNegativeDiscriminantCond(cond) {
|
|
904
|
-
if (cond.kind === "binop" && cond.op === "!==" && cond.right.kind === "str" &&
|
|
905
|
-
cond.left.kind === "field" && cond.left.isDiscriminant &&
|
|
906
|
-
cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") {
|
|
907
|
-
return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
908
|
-
}
|
|
909
|
-
// Pattern: !Array.isArray(x) — narrows x to the NonArrayBranch variant.
|
|
910
|
-
// Same var-scrutinee restriction as parseDiscriminantCond.
|
|
911
|
-
if (cond.kind === "unop" && cond.op === "!") {
|
|
912
|
-
const arrCheck = parseArrayIsArrayCall(cond.expr);
|
|
913
|
-
if (arrCheck && arrCheck.scrutinee.kind === "var") {
|
|
914
|
-
return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: "NonArrayBranch" };
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
return null;
|
|
918
|
-
}
|
|
919
|
-
function isTerminating(stmts) {
|
|
920
|
-
if (stmts.length === 0)
|
|
921
|
-
return false;
|
|
922
|
-
return isTerminatorKind(stmts[stmts.length - 1].kind);
|
|
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 };
|
|
974
|
-
}
|
|
975
|
-
/** Rule (list-level): `if (x.kind !== "v") terminate; rest` → tagMatch
|
|
976
|
-
* with cases = [{ variant: v, body: rest }] and fallthrough = terminate. */
|
|
977
|
-
function ruleDiscriminantNegEarlyReturn(stmts) {
|
|
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 };
|
|
510
|
+
return presentMatchStmts(check, [walkStmt(innerStmt, ctx)], []);
|
|
991
511
|
}
|
|
992
|
-
/**
|
|
993
|
-
*
|
|
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" },
|
|
1098
620
|
};
|
|
1099
621
|
}
|
|
1100
|
-
/**
|
|
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,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
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
|
}
|