lemmascript 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -13
- package/package.json +4 -1
- package/tools/dist/dafny-commands.js +31 -14
- package/tools/dist/dafny-emit.js +302 -17
- package/tools/dist/extract.js +1087 -181
- package/tools/dist/info-command.js +38 -0
- package/tools/dist/lean-emit.js +81 -5
- package/tools/dist/lsc.js +29 -9
- package/tools/dist/narrow.js +932 -0
- package/tools/dist/peephole.js +451 -0
- package/tools/dist/resolve.js +680 -258
- package/tools/dist/specparser.js +18 -2
- package/tools/dist/transform.js +597 -441
- package/tools/dist/types.js +128 -69
package/tools/dist/transform.js
CHANGED
|
@@ -21,10 +21,11 @@ function mapExpr(e, f) {
|
|
|
21
21
|
case "num":
|
|
22
22
|
case "bool":
|
|
23
23
|
case "str":
|
|
24
|
-
case "constructor":
|
|
25
24
|
case "emptyMap":
|
|
26
25
|
case "emptySet":
|
|
27
26
|
case "havoc": return e;
|
|
27
|
+
case "mapLiteral": return { ...e, entries: e.entries.map(en => ({ key: r(en.key), value: r(en.value) })) };
|
|
28
|
+
case "constructor": return e.args ? { ...e, args: e.args.map(r) } : e;
|
|
28
29
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
29
30
|
case "unop": return { ...e, expr: r(e.expr) };
|
|
30
31
|
case "implies": return { ...e, premises: e.premises.map(r), conclusion: r(e.conclusion) };
|
|
@@ -80,7 +81,6 @@ function mapTExpr(e, f) {
|
|
|
80
81
|
case "num":
|
|
81
82
|
case "str":
|
|
82
83
|
case "bool":
|
|
83
|
-
case "result":
|
|
84
84
|
case "havoc": return e;
|
|
85
85
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
86
86
|
case "unop": return { ...e, expr: r(e.expr) };
|
|
@@ -90,6 +90,15 @@ function mapTExpr(e, f) {
|
|
|
90
90
|
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
91
91
|
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
92
92
|
case "conditional": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
|
|
93
|
+
case "optChain": return { ...e, obj: r(e.obj),
|
|
94
|
+
chain: e.chain.map(s => s.kind === "call" ? { ...s, args: s.args.map(r) }
|
|
95
|
+
: s.kind === "index" ? { ...s, idx: r(s.idx) }
|
|
96
|
+
: s) };
|
|
97
|
+
case "nullish": return { ...e, left: r(e.left), right: r(e.right) };
|
|
98
|
+
case "someMatch": return { ...e, scrutinee: r(e.scrutinee), someBody: r(e.someBody), noneBody: r(e.noneBody) };
|
|
99
|
+
case "tagMatch": return { ...e, scrutinee: r(e.scrutinee),
|
|
100
|
+
cases: e.cases.map(c => ({ ...c, body: r(c.body) })),
|
|
101
|
+
fallthrough: e.fallthrough ? r(e.fallthrough) : null };
|
|
93
102
|
case "forall": return { ...e, body: r(e.body) };
|
|
94
103
|
case "exists": return { ...e, body: r(e.body) };
|
|
95
104
|
case "lambda": return e;
|
|
@@ -113,6 +122,10 @@ function mapTStmt(s, f) {
|
|
|
113
122
|
case "ghostLet": return { ...s, init: r(s.init) };
|
|
114
123
|
case "ghostAssign": return { ...s, value: r(s.value) };
|
|
115
124
|
case "assert": return { ...s, expr: r(s.expr) };
|
|
125
|
+
case "someMatch": return { ...s, scrutinee: r(s.scrutinee), someBody: s.someBody.map(t => mapTStmt(t, f)), noneBody: s.noneBody.map(t => mapTStmt(t, f)) };
|
|
126
|
+
case "tagMatch": return { ...s, scrutinee: r(s.scrutinee),
|
|
127
|
+
cases: s.cases.map(c => ({ ...c, body: c.body.map(t => mapTStmt(t, f)) })),
|
|
128
|
+
fallthrough: s.fallthrough.map(t => mapTStmt(t, f)) };
|
|
116
129
|
}
|
|
117
130
|
}
|
|
118
131
|
export const LEAN_OPTIONS = {
|
|
@@ -174,6 +187,40 @@ const BOOL_OP_MAP = {
|
|
|
174
187
|
...OP_MAP, "===": "==", "!==": "!=",
|
|
175
188
|
};
|
|
176
189
|
function transformExpr(e) { return lowerExpr(e, null); }
|
|
190
|
+
/** Reduce an if/let/return-shaped statement body to a single expression, for
|
|
191
|
+
* expression-only lambda bodies. Returns null for shapes that can't be a pure
|
|
192
|
+
* expression (loops, assignments, bare side effects), so callers leave the
|
|
193
|
+
* body as statements. A `return` is terminal — statements after it are
|
|
194
|
+
* unreachable and dropped.
|
|
195
|
+
* [return e] → e
|
|
196
|
+
* [let x = e, …rest] → Expr.let(x, e, flatten(rest))
|
|
197
|
+
* [if (c) thenStmts elseStmts, …rest] → Expr.if(c, …) where each branch
|
|
198
|
+
* absorbs `rest` if it doesn't already terminate with a return. */
|
|
199
|
+
function flattenLambdaBody(stmts) {
|
|
200
|
+
if (stmts.length === 0)
|
|
201
|
+
return null;
|
|
202
|
+
const first = stmts[0];
|
|
203
|
+
const rest = stmts.slice(1);
|
|
204
|
+
if (first.kind === "return")
|
|
205
|
+
return first.value;
|
|
206
|
+
if (first.kind === "let" && !first.mutable) {
|
|
207
|
+
const body = flattenLambdaBody(rest);
|
|
208
|
+
return body === null ? null : { kind: "let", name: first.name, value: first.value, body };
|
|
209
|
+
}
|
|
210
|
+
if (first.kind === "if") {
|
|
211
|
+
const thenTerminates = flattenLambdaBody(first.then);
|
|
212
|
+
if (thenTerminates !== null) {
|
|
213
|
+
// then-branch yields a value (ends in return) → `rest` is the else path.
|
|
214
|
+
const elseExpr = flattenLambdaBody(first.else.length > 0 ? [...first.else, ...rest] : rest);
|
|
215
|
+
return elseExpr === null ? null : { kind: "if", cond: first.cond, then: thenTerminates, else: elseExpr };
|
|
216
|
+
}
|
|
217
|
+
// then-branch falls through → both branches continue into `rest`.
|
|
218
|
+
const thenExpr = flattenLambdaBody([...first.then, ...rest]);
|
|
219
|
+
const elseExpr = flattenLambdaBody(first.else.length > 0 ? [...first.else, ...rest] : rest);
|
|
220
|
+
return thenExpr === null || elseExpr === null ? null : { kind: "if", cond: first.cond, then: thenExpr, else: elseExpr };
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
177
224
|
/**
|
|
178
225
|
* Lower a typed expression to Backend IR.
|
|
179
226
|
*
|
|
@@ -183,28 +230,47 @@ function transformExpr(e) { return lowerExpr(e, null); }
|
|
|
183
230
|
* a method call can appear inline in TS. It does NOT propagate into
|
|
184
231
|
* field, index, record, forall, or exists sub-expressions.
|
|
185
232
|
*/
|
|
233
|
+
/** JS truthiness coercion for `if`/`while`/`?:` conditions.
|
|
234
|
+
* Dafny requires bool; TS treats number/string/array as truthy when non-empty.
|
|
235
|
+
* Optional conds are handled separately by narrow.ts (rewritten to someMatch). */
|
|
236
|
+
function coerceCondToBool(cond, ty) {
|
|
237
|
+
if (ty.kind === "bool")
|
|
238
|
+
return cond;
|
|
239
|
+
if (ty.kind === "int" || ty.kind === "nat")
|
|
240
|
+
return { kind: "binop", op: ">", left: cond, right: { kind: "num", value: 0 } };
|
|
241
|
+
if (ty.kind === "string" || ty.kind === "array")
|
|
242
|
+
return { kind: "binop", op: ">", left: { kind: "field", obj: cond, field: "size" }, right: { kind: "num", value: 0 } };
|
|
243
|
+
return cond;
|
|
244
|
+
}
|
|
186
245
|
/** Wrap an expression in Some/None for optional-typed conditionals.
|
|
187
246
|
* If the raw TExpr is `undefined`, emit `.none`; otherwise wrap in `Some`. */
|
|
188
247
|
function wrapOptionalBranch(expr, raw) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
248
|
+
// Set type: "Option" so Lean emits `Option.some`/`Option.none` (qualified).
|
|
249
|
+
// The dotted form `.some`/`.none` would be ambiguous in expression positions
|
|
250
|
+
// like the scrutinee of an outer match. Dafny treats `Option.Some` and bare
|
|
251
|
+
// `Some` equivalently — the qualification is harmless there.
|
|
252
|
+
if (raw.kind === "var" && raw.name === "undefined")
|
|
253
|
+
return { kind: "constructor", name: "none", type: "Option" };
|
|
254
|
+
if (raw.ty.kind === "optional")
|
|
255
|
+
return expr; // already Option<T>, don't double-wrap
|
|
256
|
+
return { kind: "constructor", name: "some", type: "Option", args: [expr] };
|
|
192
257
|
}
|
|
193
258
|
function lowerExpr(e, binds) {
|
|
194
|
-
// Monadic lifting: extract embedded method calls to let-binds
|
|
195
|
-
//
|
|
196
|
-
|
|
259
|
+
// Monadic lifting: extract embedded method calls to let-binds.
|
|
260
|
+
// `callKind: "method"` means a global var-fn call (classifyCall returns
|
|
261
|
+
// "method" only for `fn.kind === "var"`). Receiver method calls have
|
|
262
|
+
// callKind "unknown" and fall through to the regular case below where
|
|
263
|
+
// they become `methodCall`.
|
|
264
|
+
if (binds && e.kind === "call" && e.callKind === "method" && e.fn.kind === "var") {
|
|
197
265
|
const name = `_t${_liftCounter++}`;
|
|
198
|
-
const fn = e.fn.kind === "var" ? e.fn.name : `${lowerExpr(e.fn, binds)}`;
|
|
199
266
|
const args = e.args.map(a => lowerExpr(a, binds));
|
|
200
|
-
binds.push({ kind: "let-bind", name, value: { kind: "app", fn, args } });
|
|
267
|
+
binds.push({ kind: "let-bind", name, value: { kind: "app", fn: e.fn.name, args } });
|
|
201
268
|
return { kind: "var", name };
|
|
202
269
|
}
|
|
203
270
|
switch (e.kind) {
|
|
204
271
|
case "var": return { kind: "var", name: e.name };
|
|
205
272
|
case "num": return { kind: "num", value: e.value };
|
|
206
273
|
case "bool": return { kind: "bool", value: e.value };
|
|
207
|
-
case "result": return { kind: "var", name: "res" };
|
|
208
274
|
case "str":
|
|
209
275
|
if (e.ty.kind === "user")
|
|
210
276
|
return { kind: "constructor", name: e.value, type: e.ty.name };
|
|
@@ -315,10 +381,27 @@ function lowerExpr(e, binds) {
|
|
|
315
381
|
(e.left.ty.kind === "user" && e.right.ty.kind === "string"))) {
|
|
316
382
|
const left = lowerExpr(e.left, binds);
|
|
317
383
|
const right = lowerExpr(e.right, binds);
|
|
384
|
+
// `s || undefined` produces `Option<string>` — wrap the truthy branch in Some.
|
|
385
|
+
const rightIsUndef = e.right.kind === "var" && e.right.name === "undefined";
|
|
318
386
|
return {
|
|
319
387
|
kind: "if",
|
|
320
388
|
cond: { kind: "binop", op: ">", left: { kind: "field", obj: left, field: "size" }, right: { kind: "num", value: 0 } },
|
|
321
|
-
then:
|
|
389
|
+
then: rightIsUndef ? { kind: "app", fn: "Some", args: [left] } : left,
|
|
390
|
+
else: right,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// `bool || undefined` → `if bool then Some(bool) else None`. Used in
|
|
394
|
+
// optional-field initialization where the source assigns a truthy/false
|
|
395
|
+
// bool to a `T?` field. Without this, emit produces `bool || None`,
|
|
396
|
+
// which Dafny rejects (bool || Option<?> is ill-typed).
|
|
397
|
+
if (e.op === "||" && e.left.ty.kind === "bool" &&
|
|
398
|
+
e.right.kind === "var" && e.right.name === "undefined") {
|
|
399
|
+
const left = lowerExpr(e.left, binds);
|
|
400
|
+
return {
|
|
401
|
+
kind: "if",
|
|
402
|
+
cond: left,
|
|
403
|
+
then: { kind: "app", fn: "Some", args: [left] },
|
|
404
|
+
else: { kind: "var", name: "undefined" },
|
|
322
405
|
};
|
|
323
406
|
}
|
|
324
407
|
// int + string → NatToString(int) + string (string concatenation)
|
|
@@ -366,12 +449,35 @@ function lowerExpr(e, binds) {
|
|
|
366
449
|
case "index": {
|
|
367
450
|
const idx = transformExpr(e.idx);
|
|
368
451
|
if (e.obj.ty.kind === "map") {
|
|
369
|
-
|
|
452
|
+
// Mirrors the .get() → .getDirect switch at line ~453: when resolve has
|
|
453
|
+
// narrowed the index type to non-optional (via `k in m` atoms in scope),
|
|
454
|
+
// emit direct access; otherwise keep the Option-producing `get`.
|
|
455
|
+
const method = e.ty.kind !== "optional" ? "getDirect" : "get";
|
|
456
|
+
return { kind: "methodCall", obj: transformExpr(e.obj), objTy: e.obj.ty, method, args: [idx], monadic: false };
|
|
370
457
|
}
|
|
371
458
|
const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
|
|
372
459
|
return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
|
|
373
460
|
}
|
|
374
461
|
case "call": {
|
|
462
|
+
// Array.isArray(x) on a synth array-union (discriminant "__isArray__")
|
|
463
|
+
// → constructor predicate `x.ArrayBranch?`. Used in spec ensures and
|
|
464
|
+
// anywhere `Array.isArray` escapes the narrowing rule (narrow rewrites
|
|
465
|
+
// top-level if-cond Array.isArray uses; this catches the rest).
|
|
466
|
+
if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Array" &&
|
|
467
|
+
e.fn.field === "isArray" && e.args.length === 1) {
|
|
468
|
+
const arg = e.args[0];
|
|
469
|
+
if (arg.ty.kind === "user") {
|
|
470
|
+
const baseName = arg.ty.name.includes("<") ? arg.ty.name.slice(0, arg.ty.name.indexOf("<")) : arg.ty.name;
|
|
471
|
+
const decl = _typeDecls.find(d => d.name === baseName);
|
|
472
|
+
if (decl?.kind === "discriminated-union" && decl.discriminant === "__isArray__") {
|
|
473
|
+
return {
|
|
474
|
+
kind: "binop", op: "=",
|
|
475
|
+
left: lowerExpr(arg, binds),
|
|
476
|
+
right: { kind: "constructor", name: "ArrayBranch", type: arg.ty.name },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
375
481
|
// Math.abs/min/max → preamble functions
|
|
376
482
|
if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math") {
|
|
377
483
|
if (e.fn.field === "abs" && e.args.length === 1)
|
|
@@ -408,6 +514,10 @@ function lowerExpr(e, binds) {
|
|
|
408
514
|
return { kind: "toNat", expr: lowered };
|
|
409
515
|
return lowered;
|
|
410
516
|
});
|
|
517
|
+
// arr.concat(otherArr): array argument → real concatenation, not push
|
|
518
|
+
if (method === "concat" && e.fn.obj.ty.kind === "array" && e.args.length === 1 && e.args[0].ty.kind === "array") {
|
|
519
|
+
return { kind: "binop", op: "arrayConcat", left: recv, right: args[0] };
|
|
520
|
+
}
|
|
411
521
|
// Spec-context map get: result type is non-optional → direct access
|
|
412
522
|
if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
|
|
413
523
|
method = "getDirect";
|
|
@@ -507,6 +617,19 @@ function lowerExpr(e, binds) {
|
|
|
507
617
|
if (e.fields.length === 0 && !e.spread && e.ty.kind === "map") {
|
|
508
618
|
return { kind: "emptyMap" };
|
|
509
619
|
}
|
|
620
|
+
// Non-empty record literal with map type — emit as a flat Dafny map
|
|
621
|
+
// literal `map[k1 := v1, k2 := v2, ...]`. (A chain of `m["k" := v]`
|
|
622
|
+
// works for a handful of entries but Dafny's type resolver stack-
|
|
623
|
+
// overflows on hundreds; the flat form is fine at any size.)
|
|
624
|
+
if (e.fields.length > 0 && !e.spread && e.ty.kind === "map") {
|
|
625
|
+
return {
|
|
626
|
+
kind: "mapLiteral",
|
|
627
|
+
entries: e.fields.map(f => ({
|
|
628
|
+
key: { kind: "str", value: f.name },
|
|
629
|
+
value: lowerExpr(f.value, binds),
|
|
630
|
+
})),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
510
633
|
return { kind: "record", spread: null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
|
|
511
634
|
}
|
|
512
635
|
case "arrayLiteral":
|
|
@@ -518,73 +641,29 @@ function lowerExpr(e, binds) {
|
|
|
518
641
|
if (e.ty.kind === "set")
|
|
519
642
|
return { kind: "app", fn: "SetLiteral", args: e.elems.map(el => lowerExpr(el, binds)) };
|
|
520
643
|
return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
|
|
521
|
-
case "lambda":
|
|
522
|
-
|
|
644
|
+
case "lambda": {
|
|
645
|
+
const body = transformStmts(e.body, []);
|
|
646
|
+
// Flatten an if/let/return-shaped multi-statement body into a single
|
|
647
|
+
// `return <expr>` so both backends' single-return-lambda fast path emits
|
|
648
|
+
// it (Dafny lambdas are expression-only; Lean prefers the expression form
|
|
649
|
+
// over a `do` block). Bodies with shapes we can't reduce (loops, bare
|
|
650
|
+
// side effects) are left as-is.
|
|
651
|
+
const flat = flattenLambdaBody(body);
|
|
652
|
+
return {
|
|
653
|
+
kind: "lambda",
|
|
654
|
+
params: e.params.map(p => ({ name: p.name, type: p.ty })),
|
|
655
|
+
body: flat === null ? body : [{ kind: "return", value: flat }],
|
|
656
|
+
};
|
|
657
|
+
}
|
|
523
658
|
case "forall":
|
|
524
659
|
return { kind: "forall", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
525
660
|
case "exists":
|
|
526
661
|
return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
527
662
|
case "conditional": {
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const bound = matchBinder(e.narrowedVar);
|
|
533
|
-
let thenExpr = lowerExpr(e.then, binds);
|
|
534
|
-
let elseExpr = lowerExpr(e.else, binds);
|
|
535
|
-
if (bound !== e.narrowedVar) {
|
|
536
|
-
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
537
|
-
}
|
|
538
|
-
if (e.ty.kind === "optional") {
|
|
539
|
-
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
540
|
-
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
541
|
-
}
|
|
542
|
-
return {
|
|
543
|
-
kind: "match", scrutinee,
|
|
544
|
-
arms: [
|
|
545
|
-
{ pattern: `.some ${bound}`, body: thenExpr },
|
|
546
|
-
{ pattern: ".none", body: elseExpr },
|
|
547
|
-
],
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
// Phase 1: Truthiness — cond itself is optional (e.g. opt ? X : Y)
|
|
551
|
-
// Uses narrowedVar set by resolve's Phase 1 (unchanged).
|
|
552
|
-
if (e.narrowedVar && e.cond.ty.kind === "optional") {
|
|
553
|
-
const cond = lowerExpr(e.cond, binds);
|
|
554
|
-
let thenExpr = lowerExpr(e.then, binds);
|
|
555
|
-
let elseExpr = lowerExpr(e.else, binds);
|
|
556
|
-
const bound = matchBinder(e.narrowedVar);
|
|
557
|
-
if (bound !== e.narrowedVar) {
|
|
558
|
-
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
559
|
-
}
|
|
560
|
-
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
561
|
-
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
562
|
-
return {
|
|
563
|
-
kind: "match", scrutinee: cond,
|
|
564
|
-
arms: [
|
|
565
|
-
{ pattern: `.some ${bound}`, body: thenExpr },
|
|
566
|
-
{ pattern: ".none", body: elseExpr },
|
|
567
|
-
],
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
// Phase 2: Explicit check — x !== undefined ? A : B
|
|
571
|
-
// Transform detects the pattern itself (no narrowedVar/narrowedExpr from resolve).
|
|
572
|
-
const check = parseOptionalCheck(e.cond);
|
|
573
|
-
if (check && !check.negated) {
|
|
574
|
-
return lowerOptionalConditional(e, check, null, binds);
|
|
575
|
-
}
|
|
576
|
-
// Phase 3: && with optional check — x !== undefined && guard(x) ? A : B
|
|
577
|
-
if (e.cond.kind === "binop" && e.cond.op === "&&") {
|
|
578
|
-
const extracted = extractLeftmostOptional(e.cond);
|
|
579
|
-
if (extracted) {
|
|
580
|
-
const innerCheck = parseOptionalCheck(extracted.optCond);
|
|
581
|
-
if (innerCheck && !innerCheck.negated) {
|
|
582
|
-
return lowerOptionalConditional(e, innerCheck, extracted.rest, binds);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
// Phase 4: Regular conditional (no optional narrowing)
|
|
587
|
-
const cond = lowerExpr(e.cond, binds);
|
|
663
|
+
// JS truthiness coercion (string/array/int → ... > 0). Matches SPEC §3.1
|
|
664
|
+
// negation forms (`!s` → `s == ""`). Optional conds are already
|
|
665
|
+
// rewritten to someMatch by narrow.ts.
|
|
666
|
+
const cond = coerceCondToBool(lowerExpr(e.cond, binds), e.cond.ty);
|
|
588
667
|
let thenExpr = lowerExpr(e.then, binds);
|
|
589
668
|
let elseExpr = lowerExpr(e.else, binds);
|
|
590
669
|
if (e.ty.kind === "optional") {
|
|
@@ -593,6 +672,12 @@ function lowerExpr(e, binds) {
|
|
|
593
672
|
}
|
|
594
673
|
return { kind: "if", cond, then: thenExpr, else: elseExpr };
|
|
595
674
|
}
|
|
675
|
+
case "optChain":
|
|
676
|
+
// Narrow should have rewritten optChain to someMatch.
|
|
677
|
+
throw new Error(`optChain reached transform — narrow should have rewritten it`);
|
|
678
|
+
case "nullish":
|
|
679
|
+
// Narrow should have rewritten nullish to someMatch.
|
|
680
|
+
throw new Error(`nullish reached transform — narrow should have rewritten it`);
|
|
596
681
|
case "havoc":
|
|
597
682
|
// Dafny's * only works in var/assign positions — lift to own declaration
|
|
598
683
|
if (binds) {
|
|
@@ -601,6 +686,82 @@ function lowerExpr(e, binds) {
|
|
|
601
686
|
return { kind: "var", name };
|
|
602
687
|
}
|
|
603
688
|
return { kind: "havoc", type: e.ty };
|
|
689
|
+
case "someMatch": {
|
|
690
|
+
let someBody;
|
|
691
|
+
let scrutinee;
|
|
692
|
+
const path = asTAccessPath(e.scrutinee);
|
|
693
|
+
if (path) {
|
|
694
|
+
// Pure access path (var or any depth of obj.f.g.h) — substitute the
|
|
695
|
+
// path with the binder pre-lowering.
|
|
696
|
+
const replaced = replacePathInTExpr(e.someBody, path, e.binder, e.binderTy);
|
|
697
|
+
someBody = lowerExpr(replaced, binds);
|
|
698
|
+
// Bare-var shortcut, but route \result through lowerExpr so the
|
|
699
|
+
// lemma-side replaceVar pass can substitute it with the function call.
|
|
700
|
+
scrutinee = path.fields.length === 0 && path.rootVar !== "\\result"
|
|
701
|
+
? path.rootVar
|
|
702
|
+
: lowerExpr(e.scrutinee, binds);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
// Complex scrutinee — narrow pre-bound the someBody to use the binder directly,
|
|
706
|
+
// so no substitution needed. Used by optChain rewrites.
|
|
707
|
+
someBody = lowerExpr(e.someBody, binds);
|
|
708
|
+
scrutinee = lowerExpr(e.scrutinee, binds);
|
|
709
|
+
}
|
|
710
|
+
let noneBody = lowerExpr(e.noneBody, binds);
|
|
711
|
+
if (e.ty.kind === "optional") {
|
|
712
|
+
someBody = wrapOptionalBranch(someBody, e.someBody);
|
|
713
|
+
noneBody = wrapOptionalBranch(noneBody, e.noneBody);
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
kind: "match", scrutinee,
|
|
717
|
+
arms: [
|
|
718
|
+
{ pattern: `.some ${e.binder}`, body: someBody },
|
|
719
|
+
{ pattern: ".none", body: noneBody },
|
|
720
|
+
],
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
case "tagMatch": {
|
|
724
|
+
// Expression-form tagMatch — emitted by `ruleImplArrayIsArray` for spec
|
|
725
|
+
// implications like `Array.isArray(x) ==> B` and by
|
|
726
|
+
// `ruleConditionalArrayIsArray` for ternary narrowing. Substitutes
|
|
727
|
+
// scrutinee field accesses and (for synth array-unions) scrutinee
|
|
728
|
+
// path occurrences inside each arm with the variant's payload binder.
|
|
729
|
+
// Path scrutinees (e.g. `m.content`) get a synthesized hint derived
|
|
730
|
+
// from the last field/var name so the binder reads naturally.
|
|
731
|
+
const scrutinee = lowerExpr(e.scrutinee, binds);
|
|
732
|
+
const decl = _typeDecls.find(d => d.name === e.typeName);
|
|
733
|
+
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
734
|
+
const varName = e.scrutinee.kind === "var" ? e.scrutinee.name : undefined;
|
|
735
|
+
const pathHint = varName ?? scrutineeHint(e.scrutinee);
|
|
736
|
+
const wrapOpt = e.ty.kind === "optional";
|
|
737
|
+
const arms = e.cases.map(c => {
|
|
738
|
+
const variant = decl?.variants?.find(v => v.name === c.variant);
|
|
739
|
+
const fields = variant?.fields ?? [];
|
|
740
|
+
let body = lowerExpr(c.body, binds);
|
|
741
|
+
if (varName && fields.length > 0) {
|
|
742
|
+
body = replaceFieldAccess(body, varName, fields);
|
|
743
|
+
if (isSynthArrayUnion && fields.length === 1) {
|
|
744
|
+
body = replaceVarInExpr(body, varName, matchBinder(fields[0].name, varName));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else if (!varName && isSynthArrayUnion && fields.length === 1) {
|
|
748
|
+
// Path scrutinee (e.g. `m.content`): replace structural occurrences
|
|
749
|
+
// with the binder var ref.
|
|
750
|
+
const binderName = matchBinder(fields[0].name, pathHint);
|
|
751
|
+
body = replaceExprInExpr(body, scrutinee, { kind: "var", name: binderName });
|
|
752
|
+
}
|
|
753
|
+
if (wrapOpt)
|
|
754
|
+
body = wrapOptionalBranch(body, c.body);
|
|
755
|
+
return { pattern: buildMatchPattern(c.variant, fields, pathHint), body };
|
|
756
|
+
});
|
|
757
|
+
if (e.fallthrough) {
|
|
758
|
+
let body = lowerExpr(e.fallthrough, binds);
|
|
759
|
+
if (wrapOpt)
|
|
760
|
+
body = wrapOptionalBranch(body, e.fallthrough);
|
|
761
|
+
arms.push({ pattern: "_", body });
|
|
762
|
+
}
|
|
763
|
+
return { kind: "match", scrutinee: varName ?? scrutinee, arms };
|
|
764
|
+
}
|
|
604
765
|
}
|
|
605
766
|
}
|
|
606
767
|
function flattenImpl(e) {
|
|
@@ -654,33 +815,124 @@ function replaceFieldAccess(e, varName, fields) {
|
|
|
654
815
|
return null;
|
|
655
816
|
});
|
|
656
817
|
}
|
|
818
|
+
/** Replace bare `var(oldName)` references → `var(newName)` in lowered IR.
|
|
819
|
+
* Used inside synth array-union match arms: the user code refers to the
|
|
820
|
+
* scrutinee by its bare name (`content`), but in the arm body that name
|
|
821
|
+
* must refer to the variant's sole payload binder (`i_content_arr`). */
|
|
822
|
+
function replaceVarInExpr(e, oldName, newName) {
|
|
823
|
+
return mapExpr(e, x => {
|
|
824
|
+
if (x.kind === "var" && x.name === oldName)
|
|
825
|
+
return { kind: "var", name: newName };
|
|
826
|
+
// If a binding shadows the name, stop substituting inside its body.
|
|
827
|
+
if (x.kind === "let" && x.name === oldName)
|
|
828
|
+
return { ...x, value: replaceVarInExpr(x.value, oldName, newName) };
|
|
829
|
+
return null;
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
/** Structural equality on Expr access-paths (var / field chain). Enough to
|
|
833
|
+
* match the scrutinee `m.content` against later occurrences in a match arm. */
|
|
834
|
+
function exprPathEqual(a, b) {
|
|
835
|
+
if (a.kind !== b.kind)
|
|
836
|
+
return false;
|
|
837
|
+
if (a.kind === "var" && b.kind === "var")
|
|
838
|
+
return a.name === b.name;
|
|
839
|
+
if (a.kind === "field" && b.kind === "field")
|
|
840
|
+
return a.field === b.field && exprPathEqual(a.obj, b.obj);
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
/** Substitute every occurrence of `target` (an access-path Expr) with `repl`
|
|
844
|
+
* inside `e`. Mirror of `replaceVarInExpr` but keyed on a sub-path rather
|
|
845
|
+
* than a bare name — needed when the narrowing scrutinee is `m.content`
|
|
846
|
+
* (field access) rather than a bare `content` (var). */
|
|
847
|
+
function replaceExprInExpr(e, target, repl) {
|
|
848
|
+
return mapExpr(e, x => {
|
|
849
|
+
if (exprPathEqual(x, target))
|
|
850
|
+
return repl;
|
|
851
|
+
return null;
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
/** Extract a short reader-friendly hint for a TExpr access-path: the last
|
|
855
|
+
* field name in a field chain, or the var name. Used to derive a stable
|
|
856
|
+
* binder prefix when the scrutinee isn't a bare var. */
|
|
857
|
+
function scrutineeHint(e) {
|
|
858
|
+
if (e.kind === "var")
|
|
859
|
+
return e.name;
|
|
860
|
+
if (e.kind === "field")
|
|
861
|
+
return e.field;
|
|
862
|
+
return "x";
|
|
863
|
+
}
|
|
657
864
|
// ── Transform statements ─────────────────────────────────────
|
|
865
|
+
// `if (X) continue; rest` → `if (!X) { rest }` at the top of a loop body.
|
|
866
|
+
// Dafny's lowered while-loops have the index increment at the bottom, so a
|
|
867
|
+
// `continue` would skip it and loop forever; rewriting to if/else lets the
|
|
868
|
+
// loop fall through normally.
|
|
869
|
+
function negateExpr(e) {
|
|
870
|
+
if (e.kind === "unop" && e.op === "!")
|
|
871
|
+
return e.expr;
|
|
872
|
+
return { kind: "unop", op: "!", expr: e };
|
|
873
|
+
}
|
|
874
|
+
/** Build the two pieces of an `arr.pop()` lowering on a named array variable:
|
|
875
|
+
* - `optValue` is `(if |arr|>0 then Some(arr[|arr|-1]) else None)` (the popped element)
|
|
876
|
+
* - `guardedTrunc` is `(if |arr|>0 then arr[..|arr|-1] else arr)` (the array minus its last element)
|
|
877
|
+
* Callers wrap these in let/assign statements appropriate to their context. */
|
|
878
|
+
function buildPopLowering(arrName, arrTy) {
|
|
879
|
+
const arrVar = { kind: "var", name: arrName };
|
|
880
|
+
const arrLen = { kind: "field", obj: arrVar, field: "size" };
|
|
881
|
+
const lastIdx = { kind: "binop", op: "-", left: arrLen, right: { kind: "num", value: 1 } };
|
|
882
|
+
const lastElem = { kind: "index", arr: arrVar, idx: lastIdx };
|
|
883
|
+
const isNonEmpty = { kind: "binop", op: ">", left: arrLen, right: { kind: "num", value: 0 } };
|
|
884
|
+
const optValue = { kind: "if", cond: isNonEmpty,
|
|
885
|
+
then: { kind: "app", fn: "Some", args: [lastElem] },
|
|
886
|
+
else: { kind: "var", name: "undefined" } };
|
|
887
|
+
const truncated = { kind: "methodCall", obj: arrVar, objTy: arrTy, method: "slice",
|
|
888
|
+
args: [{ kind: "num", value: 0 }, lastIdx], monadic: false };
|
|
889
|
+
const guardedTrunc = { kind: "if", cond: isNonEmpty, then: truncated, else: arrVar };
|
|
890
|
+
return { optValue, guardedTrunc };
|
|
891
|
+
}
|
|
892
|
+
function eliminateTopLevelContinue(stmts) {
|
|
893
|
+
const out = [];
|
|
894
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
895
|
+
const s = stmts[i];
|
|
896
|
+
// `if (X) {...A, continue}; rest` (empty else, trailing continue in then)
|
|
897
|
+
// — if A is empty, rewrite to `if (!X) { rest }`; otherwise rewrite to
|
|
898
|
+
// `if (X) { ...A } else { rest }`. Either form lets the loop fall through
|
|
899
|
+
// naturally past the bottom of the body.
|
|
900
|
+
if (s.kind === "if" && s.else.length === 0 &&
|
|
901
|
+
s.then.length >= 1 && s.then[s.then.length - 1].kind === "continue") {
|
|
902
|
+
const rest = eliminateTopLevelContinue(stmts.slice(i + 1));
|
|
903
|
+
const thenWithoutContinue = s.then.slice(0, -1);
|
|
904
|
+
if (thenWithoutContinue.length === 0) {
|
|
905
|
+
out.push({ kind: "if", cond: negateExpr(s.cond), then: rest, else: [] });
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
out.push({ kind: "if", cond: s.cond, then: thenWithoutContinue, else: rest });
|
|
909
|
+
}
|
|
910
|
+
return out;
|
|
911
|
+
}
|
|
912
|
+
// narrow.ts's ruleEarlyReturnConsume rewrites `if (!x) continue; rest`
|
|
913
|
+
// (when x is Optional) to a someMatch which transform.ts then emits as a
|
|
914
|
+
// `match`. A trailing `continue` inside a match arm is a no-op when the
|
|
915
|
+
// match is the last statement in the loop body — drop it.
|
|
916
|
+
if (s.kind === "match" && i === stmts.length - 1) {
|
|
917
|
+
const arms = s.arms.map(arm => {
|
|
918
|
+
const b = arm.body;
|
|
919
|
+
if (b.length > 0 && b[b.length - 1].kind === "continue") {
|
|
920
|
+
return { ...arm, body: b.slice(0, -1) };
|
|
921
|
+
}
|
|
922
|
+
return arm;
|
|
923
|
+
});
|
|
924
|
+
out.push({ ...s, arms });
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
out.push(s);
|
|
928
|
+
}
|
|
929
|
+
return out;
|
|
930
|
+
}
|
|
658
931
|
function transformStmts(stmts, typeDecls) {
|
|
659
932
|
const result = [];
|
|
660
933
|
let i = 0;
|
|
661
934
|
while (i < stmts.length) {
|
|
662
935
|
const s = stmts[i];
|
|
663
|
-
// Detect discriminant if-chain → match
|
|
664
|
-
if (s.kind === "if") {
|
|
665
|
-
const chain = detectDiscriminantChain(stmts.slice(i));
|
|
666
|
-
if (chain) {
|
|
667
|
-
result.push(emitMatchStmt(chain.chain, typeDecls));
|
|
668
|
-
i += chain.consumed;
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
// Detect optional check → match on Some/None
|
|
672
|
-
const optMatch = prepareOptionalMatch(s, stmts.slice(i + 1));
|
|
673
|
-
if (optMatch) {
|
|
674
|
-
result.push(emitOptionalMatch(optMatch.check.varName, optMatch.check.negated, s, typeDecls, stmts.slice(i + 1), optMatch.check.fieldExpr));
|
|
675
|
-
// If rest was consumed into the Some branch, skip remaining
|
|
676
|
-
const origSome = optMatch.check.negated ? s.else : s.then;
|
|
677
|
-
if (origSome.length === 0 && i + 1 < stmts.length) {
|
|
678
|
-
return result;
|
|
679
|
-
}
|
|
680
|
-
i++;
|
|
681
|
-
continue;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
936
|
// Transform for-of → for-in over range
|
|
685
937
|
if (s.kind === "forof") {
|
|
686
938
|
const varName = s.names[0];
|
|
@@ -700,7 +952,7 @@ function transformStmts(stmts, typeDecls) {
|
|
|
700
952
|
const idxName = `_${keyName}_idx${suffix}`;
|
|
701
953
|
const idx = { kind: "var", name: idxName };
|
|
702
954
|
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
703
|
-
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
955
|
+
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
704
956
|
const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
|
|
705
957
|
const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
|
|
706
958
|
result.push({
|
|
@@ -726,7 +978,7 @@ function transformStmts(stmts, typeDecls) {
|
|
|
726
978
|
const idxName = `_${keyName}_idx${suffix}`;
|
|
727
979
|
const idx = { kind: "var", name: idxName };
|
|
728
980
|
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
729
|
-
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
981
|
+
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
730
982
|
const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
|
|
731
983
|
const letVal = { kind: "let", name: valueName, type: valueTy, mutable: false,
|
|
732
984
|
value: { kind: "methodCall", obj: iterExpr, objTy: s.iterable.ty, method: "getDirect", args: [{ kind: "var", name: keyName }], monadic: false } };
|
|
@@ -753,7 +1005,7 @@ function transformStmts(stmts, typeDecls) {
|
|
|
753
1005
|
const idxName = `_${varName}_idx${suffix}`;
|
|
754
1006
|
const idx = { kind: "var", name: idxName };
|
|
755
1007
|
const arrSize = { kind: "field", obj: iterExpr, field: "size" };
|
|
756
|
-
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
1008
|
+
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
757
1009
|
const letElem = { kind: "let", name: varName, type: varTy, mutable: false, value: { kind: "index", arr: iterExpr, idx } };
|
|
758
1010
|
// Auto-add bound invariant: idx ≤ bound (always true for range loops)
|
|
759
1011
|
const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
|
|
@@ -798,6 +1050,17 @@ function transformStmt(s, typeDecls) {
|
|
|
798
1050
|
return [letHead, sliceTail];
|
|
799
1051
|
}
|
|
800
1052
|
}
|
|
1053
|
+
// let x = arr.pop() → let x: T? = (Option-expr); arr := (truncated-or-self)
|
|
1054
|
+
if (init && init.fn.kind === "field" && init.fn.field === "pop" && init.fn.obj.ty.kind === "array") {
|
|
1055
|
+
const arrName = init.fn.obj.kind === "var" ? init.fn.obj.name : undefined;
|
|
1056
|
+
if (arrName) {
|
|
1057
|
+
const { optValue, guardedTrunc } = buildPopLowering(arrName, init.fn.obj.ty);
|
|
1058
|
+
return [
|
|
1059
|
+
{ kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: optValue },
|
|
1060
|
+
{ kind: "assign", target: arrName, value: guardedTrunc },
|
|
1061
|
+
];
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
801
1064
|
// new Map(arr.map(n => [n.field, n])) → let m = map[]; for (n of arr) m[n.field] := n
|
|
802
1065
|
if (init && init.fn.kind === "var" && init.fn.name === "__mapFromArray" &&
|
|
803
1066
|
init.args.length === 1 && init.args[0].kind === "call" &&
|
|
@@ -844,25 +1107,21 @@ function transformStmt(s, typeDecls) {
|
|
|
844
1107
|
return stmts;
|
|
845
1108
|
}
|
|
846
1109
|
}
|
|
847
|
-
// Desugar: const x = optCheck && guard ? A : B
|
|
848
|
-
// → var x := B; if (optCheck && guard) { x := A; }
|
|
849
|
-
// This avoids putting method calls (from guard) inside a match expression,
|
|
850
|
-
// which Dafny doesn't allow. The if-case in transformStmt handles the &&
|
|
851
|
-
// via extractLeftmostOptional → emitOptionalMatch (statement-level match).
|
|
852
|
-
if (s.init.kind === "conditional" && s.init.cond.kind === "binop" && s.init.cond.op === "&&") {
|
|
853
|
-
const extracted = extractLeftmostOptional(s.init.cond);
|
|
854
|
-
if (extracted) {
|
|
855
|
-
const desugared = [
|
|
856
|
-
{ kind: "let", name: s.name, ty: s.ty, mutable: true, init: s.init.else },
|
|
857
|
-
{ kind: "if", cond: s.init.cond, then: [{ kind: "assign", target: s.name, value: s.init.then }], else: [] },
|
|
858
|
-
];
|
|
859
|
-
return desugared.flatMap(ds => transformStmt(ds, typeDecls));
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
1110
|
const { binds, expr } = liftMethodCalls(s.init);
|
|
863
1111
|
return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
|
|
864
1112
|
}
|
|
865
1113
|
case "assign": {
|
|
1114
|
+
// x = arr.pop() → x := (Option-expr); arr := (truncated-or-self)
|
|
1115
|
+
if (s.value.kind === "call" && s.value.fn.kind === "field" &&
|
|
1116
|
+
s.value.fn.field === "pop" && s.value.fn.obj.ty.kind === "array" &&
|
|
1117
|
+
s.value.fn.obj.kind === "var") {
|
|
1118
|
+
const arrName = s.value.fn.obj.name;
|
|
1119
|
+
const { optValue, guardedTrunc } = buildPopLowering(arrName, s.value.fn.obj.ty);
|
|
1120
|
+
return [
|
|
1121
|
+
{ kind: "assign", target: s.target, value: optValue },
|
|
1122
|
+
{ kind: "assign", target: arrName, value: guardedTrunc },
|
|
1123
|
+
];
|
|
1124
|
+
}
|
|
866
1125
|
// Top-level method call → direct monadic bind, no lifting needed
|
|
867
1126
|
if (s.value.kind === "call" && s.value.callKind === "method")
|
|
868
1127
|
return [{ kind: "bind", target: s.target, value: transformExpr(s.value) }];
|
|
@@ -889,15 +1148,19 @@ function transformStmt(s, typeDecls) {
|
|
|
889
1148
|
const { binds, expr } = liftMethodCalls(s.expr);
|
|
890
1149
|
return [...binds, { kind: "assign", target: receiver, value: expr }];
|
|
891
1150
|
}
|
|
892
|
-
// Optional chaining on map.get: m.get(k)?.push(v)
|
|
893
|
-
if
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1151
|
+
// Optional chaining on map.get at statement level: m.get(k)?.push(v)
|
|
1152
|
+
// → if k in m { m[k] := m[k] + [v] } (actual mutation, not value-discard).
|
|
1153
|
+
// Narrow rewrote this to a someMatch — destructure to find the underlying
|
|
1154
|
+
// m.get(k) scrutinee and the .push(v) body call.
|
|
1155
|
+
if (s.expr.kind === "someMatch" &&
|
|
1156
|
+
s.expr.scrutinee.kind === "call" && s.expr.scrutinee.fn.kind === "field" &&
|
|
1157
|
+
s.expr.scrutinee.fn.field === "get" && s.expr.scrutinee.fn.obj.ty.kind === "map" &&
|
|
1158
|
+
s.expr.someBody.kind === "call" && s.expr.someBody.fn.kind === "field" &&
|
|
1159
|
+
s.expr.someBody.fn.field === "push") {
|
|
1160
|
+
const mapExpr = s.expr.scrutinee.fn.obj;
|
|
898
1161
|
const mapName = mapExpr.kind === "var" ? mapExpr.name : undefined;
|
|
899
|
-
const keyExpr = lowerExpr(s.expr.
|
|
900
|
-
const pushArg = lowerExpr(s.expr.args[0], null);
|
|
1162
|
+
const keyExpr = lowerExpr(s.expr.scrutinee.args[0], null);
|
|
1163
|
+
const pushArg = lowerExpr(s.expr.someBody.args[0], null);
|
|
901
1164
|
if (mapName) {
|
|
902
1165
|
const mapVar = { kind: "var", name: mapName };
|
|
903
1166
|
const directGet = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "getDirect", args: [keyExpr], monadic: false };
|
|
@@ -911,29 +1174,18 @@ function transformStmt(s, typeDecls) {
|
|
|
911
1174
|
return [...binds, { kind: "assign", target: "_", value: expr }];
|
|
912
1175
|
}
|
|
913
1176
|
case "if": {
|
|
914
|
-
//
|
|
915
|
-
// from a && chain and nest the rest inside. Handles left-associative chains:
|
|
916
|
-
// if ((x !== undefined && b) && c) → if (x !== undefined) { if (b && c) { ... } }
|
|
917
|
-
if (s.cond.kind === "binop" && s.cond.op === "&&" && s.else.length === 0) {
|
|
918
|
-
const extracted = extractLeftmostOptional(s.cond);
|
|
919
|
-
if (extracted) {
|
|
920
|
-
const innerIf = { kind: "if", cond: extracted.rest, then: s.then, else: [] };
|
|
921
|
-
const outerIf = { kind: "if", cond: extracted.optCond, then: [innerIf], else: [] };
|
|
922
|
-
return transformStmts([outerIf], typeDecls);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
// Lift from condition only (Lean rule: don't lift from branches)
|
|
1177
|
+
// Lift from condition only (Lean rule: don't lift from branches).
|
|
926
1178
|
const { binds, expr: cond } = liftMethodCalls(s.cond);
|
|
927
|
-
return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
1179
|
+
return [...binds, { kind: "if", cond: coerceCondToBool(cond, s.cond.ty), then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
928
1180
|
}
|
|
929
1181
|
case "while":
|
|
930
1182
|
return [{
|
|
931
1183
|
kind: "while",
|
|
932
|
-
cond: transformExpr(s.cond),
|
|
1184
|
+
cond: coerceCondToBool(transformExpr(s.cond), s.cond.ty),
|
|
933
1185
|
invariants: s.invariants.map(transformExpr),
|
|
934
1186
|
decreasing: s.decreases ? transformExpr(s.decreases) : null,
|
|
935
1187
|
doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
|
|
936
|
-
body: transformStmts(s.body, typeDecls),
|
|
1188
|
+
body: eliminateTopLevelContinue(transformStmts(s.body, typeDecls)),
|
|
937
1189
|
}];
|
|
938
1190
|
case "throw":
|
|
939
1191
|
return [{ kind: "assert", expr: { kind: "bool", value: false } }];
|
|
@@ -946,181 +1198,32 @@ function transformStmt(s, typeDecls) {
|
|
|
946
1198
|
case "ghostAssign":
|
|
947
1199
|
return [{ kind: "ghostAssign", target: s.target, value: transformExpr(s.value) }];
|
|
948
1200
|
case "assert":
|
|
949
|
-
return [{ kind: "assert", expr: transformExpr(s.expr) }];
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
return [];
|
|
967
|
-
if (s.else.length === 1 && s.else[0].kind === "if")
|
|
968
|
-
return collectElse(s.else[0]);
|
|
969
|
-
return s.else;
|
|
970
|
-
}
|
|
971
|
-
// Walk consecutive top-level ifs on the same discriminant
|
|
972
|
-
let consumed = 0;
|
|
973
|
-
for (let i = 0; i < stmts.length; i++) {
|
|
974
|
-
const s = stmts[i];
|
|
975
|
-
if (s.kind !== "if")
|
|
976
|
-
break;
|
|
977
|
-
const p = parseDiscriminantCond(s.cond);
|
|
978
|
-
if (!p || p.varName !== first.varName)
|
|
979
|
-
break;
|
|
980
|
-
cases.push({ variant: p.variant, body: s.then });
|
|
981
|
-
consumed = i + 1;
|
|
982
|
-
if (s.else.length > 0) {
|
|
983
|
-
const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
|
|
984
|
-
return cases.length > 0 ? { chain: { ...first, cases, fallthrough: ft }, consumed } : null;
|
|
1201
|
+
return [{ kind: "assert", expr: transformExpr(s.expr), assumed: s.assumed }];
|
|
1202
|
+
case "someMatch": {
|
|
1203
|
+
const path = asTAccessPath(s.scrutinee);
|
|
1204
|
+
if (path) {
|
|
1205
|
+
const replaced = replacePathInTStmts(s.someBody, path, s.binder, s.binderTy);
|
|
1206
|
+
const someBody = transformStmts(replaced, typeDecls);
|
|
1207
|
+
const noneBody = transformStmts(s.noneBody, typeDecls);
|
|
1208
|
+
const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
|
|
1209
|
+
return [{
|
|
1210
|
+
kind: "match", scrutinee,
|
|
1211
|
+
arms: [
|
|
1212
|
+
{ pattern: `.some ${s.binder}`, body: someBody },
|
|
1213
|
+
{ pattern: ".none", body: noneBody },
|
|
1214
|
+
],
|
|
1215
|
+
}];
|
|
1216
|
+
}
|
|
1217
|
+
throw new Error(`someMatch stmt scrutinee must be a pure access path, got ${s.scrutinee.kind}`);
|
|
985
1218
|
}
|
|
1219
|
+
case "tagMatch":
|
|
1220
|
+
return [emitMatchStmt(s.scrutinee, s.typeName, s.cases, s.fallthrough, typeDecls)];
|
|
986
1221
|
}
|
|
987
|
-
if (cases.length === 0)
|
|
988
|
-
return null;
|
|
989
|
-
return { chain: { ...first, cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
|
|
990
|
-
}
|
|
991
|
-
function parseDiscriminantCond(cond) {
|
|
992
|
-
// Pattern: x.discriminant === "variant"
|
|
993
|
-
if (cond.kind !== "binop" || cond.op !== "===" || cond.right.kind !== "str")
|
|
994
|
-
return null;
|
|
995
|
-
if (cond.left.kind !== "field" || !cond.left.isDiscriminant)
|
|
996
|
-
return null;
|
|
997
|
-
if (cond.left.obj.kind !== "var" || cond.left.obj.ty.kind !== "user")
|
|
998
|
-
return null;
|
|
999
|
-
return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
1000
|
-
}
|
|
1001
|
-
function emitOptionalMatch(varName, negated, s, typeDecls, restStmts, fieldExpr) {
|
|
1002
|
-
let someBranch = negated ? s.else : s.then;
|
|
1003
|
-
const noneBranch = negated ? s.then : s.else;
|
|
1004
|
-
// Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
|
|
1005
|
-
// so include remaining statements as the Some body
|
|
1006
|
-
if (someBranch.length === 0 && restStmts && restStmts.length > 0) {
|
|
1007
|
-
someBranch = restStmts;
|
|
1008
|
-
}
|
|
1009
|
-
const sanitized = varName.replace(/\./g, "_");
|
|
1010
|
-
const bound = matchBinder(`${sanitized}_val`);
|
|
1011
|
-
// Replace the narrowed variable/field in the Some branch body.
|
|
1012
|
-
// Field chains: replace in TStmt before transform (so downstream narrowing sees simple vars).
|
|
1013
|
-
// Simple vars: replace in IR after transform (the original mechanism).
|
|
1014
|
-
let someBody;
|
|
1015
|
-
if (fieldExpr && fieldExpr.kind === "field" && fieldExpr.obj.kind === "var") {
|
|
1016
|
-
const innerTy = fieldExpr.ty.kind === "optional" ? fieldExpr.ty.inner : fieldExpr.ty;
|
|
1017
|
-
const replaced = replaceFieldsInTStmts(someBranch, fieldExpr.obj.name, [
|
|
1018
|
-
{ fieldName: fieldExpr.field, newName: bound, fallbackTy: innerTy },
|
|
1019
|
-
]);
|
|
1020
|
-
someBody = transformStmts(replaced, typeDecls);
|
|
1021
|
-
}
|
|
1022
|
-
else {
|
|
1023
|
-
const transformed = transformStmts(someBranch, typeDecls);
|
|
1024
|
-
someBody = transformed.map(stmt => mapStmtExprs(stmt, e => replaceVar(e, varName, { kind: "var", name: bound }, true)));
|
|
1025
|
-
}
|
|
1026
|
-
// For field chains, use an Expr scrutinee so outer match replaceVar can
|
|
1027
|
-
// substitute the object variable (e.g. task → i_task_val in task.deletedFromList).
|
|
1028
|
-
// String scrutinees are opaque to replaceVar/mapExpr.
|
|
1029
|
-
const scrutinee = fieldExpr ? transformExpr(fieldExpr) : varName;
|
|
1030
|
-
return {
|
|
1031
|
-
kind: "match", scrutinee,
|
|
1032
|
-
arms: [
|
|
1033
|
-
{ pattern: `.some ${bound}`, body: someBody },
|
|
1034
|
-
{ pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
|
|
1035
|
-
],
|
|
1036
|
-
};
|
|
1037
1222
|
}
|
|
1038
1223
|
/** Apply an expression transform to all expressions in a statement (convenience wrapper). */
|
|
1039
1224
|
function mapStmtExprs(s, r) {
|
|
1040
1225
|
return mapStmt(s, e => r(e));
|
|
1041
1226
|
}
|
|
1042
|
-
// ── Optional narrowing helpers ──────────────────────────────
|
|
1043
|
-
//
|
|
1044
|
-
// Optional narrowing converts TS `if (x === undefined)` / `x !== undefined ? a : b`
|
|
1045
|
-
// patterns to `match x { Some(val) => ..., None => ... }`.
|
|
1046
|
-
//
|
|
1047
|
-
// The resolve phase (resolve.ts) handles TYPE narrowing only:
|
|
1048
|
-
// - Flow narrowing: after `if (x === undefined) return`, x is non-optional
|
|
1049
|
-
// - && narrowing: in `x !== undefined && f(x)`, f(x) sees x as non-optional
|
|
1050
|
-
// - Conditional type narrowing: extends env (simple vars) or narrowedFields context
|
|
1051
|
-
// (field chains) so the then-branch resolves with the unwrapped type
|
|
1052
|
-
//
|
|
1053
|
-
// The transform phase (here) handles ALL structural narrowing:
|
|
1054
|
-
// - Statement-level: `transformStmts` detects optional checks → `emitOptionalMatch`
|
|
1055
|
-
// - Expression-level: `lowerExpr` detects optional checks via `parseOptionalCheck`
|
|
1056
|
-
// and `extractLeftmostOptional` → `lowerOptionalConditional`
|
|
1057
|
-
// - && restructuring: `extractLeftmostOptional` splits `&&` chains, generating
|
|
1058
|
-
// match with guard: `match x { Some(val) => if guard then A else B, None => B }`
|
|
1059
|
-
/** Shared logic for optional match in both imperative and pure function paths.
|
|
1060
|
-
* Detects optional check, selects branches, handles early-return consumption.
|
|
1061
|
-
* Returns null if the condition is not an optional check. */
|
|
1062
|
-
function prepareOptionalMatch(s, restStmts) {
|
|
1063
|
-
const check = parseOptionalCheck(s.cond);
|
|
1064
|
-
if (!check)
|
|
1065
|
-
return null;
|
|
1066
|
-
let someBranch = check.negated ? s.else : s.then;
|
|
1067
|
-
const noneBranch = check.negated ? s.then : (s.else.length > 0 ? s.else : restStmts);
|
|
1068
|
-
// Early-return pattern: Some branch is empty → consume rest of block
|
|
1069
|
-
if (someBranch.length === 0 && restStmts.length > 0)
|
|
1070
|
-
someBranch = restStmts;
|
|
1071
|
-
const sanitized = check.varName.replace(/\./g, "_");
|
|
1072
|
-
const bound = matchBinder(`${sanitized}_val`);
|
|
1073
|
-
return { check, someBranch, noneBranch, bound };
|
|
1074
|
-
}
|
|
1075
|
-
/** Extract the leftmost optional check from a && chain, returning the check and the rest.
|
|
1076
|
-
* (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
|
|
1077
|
-
function extractLeftmostOptional(cond) {
|
|
1078
|
-
if (cond.kind !== "binop" || cond.op !== "&&")
|
|
1079
|
-
return null;
|
|
1080
|
-
const check = parseOptionalCheck(cond.left);
|
|
1081
|
-
if (check && !check.negated)
|
|
1082
|
-
return { optCond: cond.left, rest: cond.right };
|
|
1083
|
-
if (cond.left.kind === "binop" && cond.left.op === "&&") {
|
|
1084
|
-
const inner = extractLeftmostOptional(cond.left);
|
|
1085
|
-
if (inner)
|
|
1086
|
-
return { optCond: inner.optCond, rest: { ...cond, left: inner.rest } };
|
|
1087
|
-
}
|
|
1088
|
-
return null;
|
|
1089
|
-
}
|
|
1090
|
-
/** Detect `v !== undefined` or `undefined !== v` where v has optional type.
|
|
1091
|
-
* Also handles field access chains like `obj.field !== undefined`.
|
|
1092
|
-
* When `fieldExpr` is returned, callers must use field-aware replacement. */
|
|
1093
|
-
function parseOptionalCheck(cond) {
|
|
1094
|
-
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
|
|
1095
|
-
return null;
|
|
1096
|
-
let varExpr = null;
|
|
1097
|
-
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
1098
|
-
varExpr = cond.left;
|
|
1099
|
-
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
1100
|
-
varExpr = cond.right;
|
|
1101
|
-
if (!varExpr)
|
|
1102
|
-
return null;
|
|
1103
|
-
if (varExpr.kind === "var" && varExpr.ty.kind === "optional") {
|
|
1104
|
-
return { varName: varExpr.name, negated: cond.op === "===" };
|
|
1105
|
-
}
|
|
1106
|
-
if (varExpr.kind === "field" && varExpr.ty.kind === "optional") {
|
|
1107
|
-
// Serialize field chain as a dotted name for use as match scrutinee
|
|
1108
|
-
const chain = serializeFieldChain(varExpr);
|
|
1109
|
-
if (chain)
|
|
1110
|
-
return { varName: chain, negated: cond.op === "===", fieldExpr: varExpr };
|
|
1111
|
-
}
|
|
1112
|
-
return null;
|
|
1113
|
-
}
|
|
1114
|
-
/** Serialize a field access chain to a dotted variable path, or null if not a simple chain. */
|
|
1115
|
-
function serializeFieldChain(e) {
|
|
1116
|
-
if (e.kind === "var")
|
|
1117
|
-
return e.name;
|
|
1118
|
-
if (e.kind === "field") {
|
|
1119
|
-
const parent = serializeFieldChain(e.obj);
|
|
1120
|
-
return parent ? `${parent}.${e.field}` : null;
|
|
1121
|
-
}
|
|
1122
|
-
return null;
|
|
1123
|
-
}
|
|
1124
1227
|
/** Build match arms from variant cases — shared by imperative and pure paths.
|
|
1125
1228
|
* Looks up variant fields from typeDecls, builds patterns via buildMatchPattern,
|
|
1126
1229
|
* and delegates body transformation to the caller-provided function.
|
|
@@ -1139,12 +1242,84 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
|
1139
1242
|
}
|
|
1140
1243
|
return arms;
|
|
1141
1244
|
}
|
|
1142
|
-
function emitMatchStmt(
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1245
|
+
function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
|
|
1246
|
+
const decl = typeDecls.find(d => d.name === typeName);
|
|
1247
|
+
// Synth array-unions (discriminant "__isArray__") have single-field variants
|
|
1248
|
+
// ArrayBranch(arr) / NonArrayBranch(val). The matched arm refers to the
|
|
1249
|
+
// scrutinee by its bare name/path (`content`, `m.content`), not `.arr`, so
|
|
1250
|
+
// we substitute that whole reference with the variant's sole field binder.
|
|
1251
|
+
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
1252
|
+
// The scrutinee is a bare var (`current`) or a field-access path
|
|
1253
|
+
// (`current.content`). `prefix` names the binder scope — the var name, or a
|
|
1254
|
+
// safe id derived from the path (`current.content` → `current_content`) —
|
|
1255
|
+
// and is used for both pattern binders and arm-body substitution so they
|
|
1256
|
+
// always agree. A var scrutinee has empty `fields`, so `prefix` is just its
|
|
1257
|
+
// name and the emitted code is unchanged from before this generalization.
|
|
1258
|
+
const path = asTAccessPath(scrutinee);
|
|
1259
|
+
const isPath = !!path && path.fields.length > 0;
|
|
1260
|
+
const prefix = path ? [path.rootVar, ...path.fields].join("_") : "?";
|
|
1261
|
+
function transformArmBody(body, fields) {
|
|
1262
|
+
let stmts;
|
|
1263
|
+
if (isPath && path) {
|
|
1264
|
+
// Path scrutinee: the matched value is referred to by the bare path, so
|
|
1265
|
+
// substitute the whole path (only the synth single-field shape arises
|
|
1266
|
+
// here — discriminant chains require a var scrutinee).
|
|
1267
|
+
stmts = isSynthArrayUnion && fields.length === 1
|
|
1268
|
+
? replacePathInTStmts(body, path, matchBinder(fields[0].name, prefix), fields[0].type ?? parseTsType(fields[0].tsType))
|
|
1269
|
+
: body;
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
stmts = replaceFieldAccessInTStmts(body, prefix, fields);
|
|
1273
|
+
if (isSynthArrayUnion && fields.length === 1) {
|
|
1274
|
+
const f = fields[0];
|
|
1275
|
+
stmts = replaceVarInTStmts(stmts, prefix, matchBinder(f.name, prefix), f.type ?? parseTsType(f.tsType));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return transformStmts(stmts, typeDecls);
|
|
1279
|
+
}
|
|
1280
|
+
const armCases = cases.map(c => ({ name: c.variant, body: c.body }));
|
|
1281
|
+
const arms = buildMatchArms(armCases, prefix, typeName, typeDecls, (body, _vn, fields) => transformArmBody(body, fields));
|
|
1282
|
+
// Add the fallthrough arm whenever the listed cases don't cover every
|
|
1283
|
+
// variant — needed for exhaustiveness even when there's no `else`
|
|
1284
|
+
// (`fallthrough` empty), e.g. `if (Array.isArray(x)) {...}` with no else
|
|
1285
|
+
// becomes `match x { case ArrayBranch(..) => ... case NonArrayBranch(..) => }`.
|
|
1286
|
+
const allCovered = !!decl?.variants && cases.length >= decl.variants.length;
|
|
1287
|
+
if (!allCovered) {
|
|
1288
|
+
const remaining = remainingVariant(typeName, cases, typeDecls);
|
|
1289
|
+
if (remaining) {
|
|
1290
|
+
// Exactly one variant left — destructure so the fallthrough body can
|
|
1291
|
+
// access variant-specific fields (Lean requires this; Dafny tolerates `_`).
|
|
1292
|
+
const pattern = buildMatchPattern(remaining.name, remaining.fields, prefix);
|
|
1293
|
+
const body = transformArmBody(fallthrough, remaining.fields);
|
|
1294
|
+
arms.push({ pattern, body });
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
arms.push({ pattern: "_", body: transformStmts(fallthrough, typeDecls) });
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return { kind: "match", scrutinee: isPath ? transformExpr(scrutinee) : prefix, arms };
|
|
1301
|
+
}
|
|
1302
|
+
/** Replace bare `var(oldName)` references → `var(newName)` with the given type.
|
|
1303
|
+
* Used by emitMatchStmt for synth array-unions where the variant has a single
|
|
1304
|
+
* payload field and the user code refers to the scrutinee by its bare name. */
|
|
1305
|
+
function replaceVarInTStmts(stmts, oldName, newName, newTy) {
|
|
1306
|
+
return stmts.map(s => mapTStmt(s, e => {
|
|
1307
|
+
if (e.kind === "var" && e.name === oldName) {
|
|
1308
|
+
return { kind: "var", name: newName, ty: newTy };
|
|
1309
|
+
}
|
|
1310
|
+
return null;
|
|
1311
|
+
}));
|
|
1312
|
+
}
|
|
1313
|
+
/** If the chain has matched all variants but one, return that remaining variant. */
|
|
1314
|
+
function remainingVariant(typeName, cases, typeDecls) {
|
|
1315
|
+
const decl = typeDecls.find(d => d.name === typeName);
|
|
1316
|
+
if (!decl?.variants)
|
|
1317
|
+
return null;
|
|
1318
|
+
const matched = new Set(cases.map(c => c.variant));
|
|
1319
|
+
const remaining = decl.variants.filter(v => !matched.has(v.name));
|
|
1320
|
+
if (remaining.length !== 1)
|
|
1321
|
+
return null;
|
|
1322
|
+
return remaining[0];
|
|
1148
1323
|
}
|
|
1149
1324
|
function emitSwitchStmt(s, typeDecls) {
|
|
1150
1325
|
const varName = s.expr.kind === "var" ? s.expr.name : "?";
|
|
@@ -1155,10 +1330,7 @@ function emitSwitchStmt(s, typeDecls) {
|
|
|
1155
1330
|
arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
|
|
1156
1331
|
return { kind: "match", scrutinee: varName, arms };
|
|
1157
1332
|
}
|
|
1158
|
-
/** Replace obj.field → replacement var in typed IR
|
|
1159
|
-
* Used by discriminant match/switch and optional match to rewrite field accesses
|
|
1160
|
-
* into simple variables before the transform phase, so downstream narrowing
|
|
1161
|
-
* (parseOptionalCheck, extractLeftmostOptional) sees simple variable references.
|
|
1333
|
+
/** Replace obj.field → replacement var in typed IR.
|
|
1162
1334
|
* Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
|
|
1163
1335
|
function replaceFieldsInTStmts(stmts, objName, replacements) {
|
|
1164
1336
|
if (replacements.length === 0)
|
|
@@ -1199,6 +1371,40 @@ function replaceFieldInTExpr(expr, objName, replacements) {
|
|
|
1199
1371
|
return null;
|
|
1200
1372
|
});
|
|
1201
1373
|
}
|
|
1374
|
+
function asTAccessPath(e) {
|
|
1375
|
+
if (e.kind === "var")
|
|
1376
|
+
return { rootVar: e.name, fields: [] };
|
|
1377
|
+
if (e.kind === "field") {
|
|
1378
|
+
const inner = asTAccessPath(e.obj);
|
|
1379
|
+
if (!inner)
|
|
1380
|
+
return null;
|
|
1381
|
+
return { rootVar: inner.rootVar, fields: [...inner.fields, e.field] };
|
|
1382
|
+
}
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
/** Does TExpr `e` match the given access path exactly? */
|
|
1386
|
+
function matchesAccessPath(e, path) {
|
|
1387
|
+
const collected = [];
|
|
1388
|
+
let cur = e;
|
|
1389
|
+
while (cur.kind === "field") {
|
|
1390
|
+
collected.unshift(cur.field);
|
|
1391
|
+
cur = cur.obj;
|
|
1392
|
+
}
|
|
1393
|
+
if (cur.kind !== "var" || cur.name !== path.rootVar)
|
|
1394
|
+
return false;
|
|
1395
|
+
if (collected.length !== path.fields.length)
|
|
1396
|
+
return false;
|
|
1397
|
+
return collected.every((f, i) => f === path.fields[i]);
|
|
1398
|
+
}
|
|
1399
|
+
/** Replace every TExpr matching `path` with `var(binder, binderTy)`. */
|
|
1400
|
+
function replacePathInTExpr(expr, path, binder, binderTy) {
|
|
1401
|
+
return mapTExpr(expr, e => matchesAccessPath(e, path)
|
|
1402
|
+
? { kind: "var", name: binder, ty: binderTy } : null);
|
|
1403
|
+
}
|
|
1404
|
+
function replacePathInTStmts(stmts, path, binder, binderTy) {
|
|
1405
|
+
return stmts.map(s => mapTStmt(s, e => matchesAccessPath(e, path)
|
|
1406
|
+
? { kind: "var", name: binder, ty: binderTy } : null));
|
|
1407
|
+
}
|
|
1202
1408
|
/** Unwrap optional type on match-bound variables in TExpr.
|
|
1203
1409
|
* After replaceFieldInTExpr, the replaced variable carries the original optional
|
|
1204
1410
|
* type from the field declaration. The match binding unwraps it to the inner type. */
|
|
@@ -1206,80 +1412,14 @@ function fixBoundType(expr, boundName) {
|
|
|
1206
1412
|
return mapTExpr(expr, e => e.kind === "var" && e.name === boundName && e.ty.kind === "optional"
|
|
1207
1413
|
? { ...e, ty: e.ty.inner } : null);
|
|
1208
1414
|
}
|
|
1209
|
-
/** Lower a conditional with an optional check (Phase 2: explicit, Phase 3: && with guard).
|
|
1210
|
-
* Generates: match scrutinee { Some(val) => [if guard then] A [else B], None => B }
|
|
1211
|
-
* For field chains, replaces field accesses in TExpr before lowering.
|
|
1212
|
-
* For simple vars, replaces variable names in Expr after lowering. */
|
|
1213
|
-
function lowerOptionalConditional(e, check, guard, binds) {
|
|
1214
|
-
const sanitized = check.varName.replace(/\./g, "_");
|
|
1215
|
-
const bound = matchBinder(`${sanitized}_val`);
|
|
1216
|
-
const isFieldChain = check.fieldExpr && check.fieldExpr.kind === "field" &&
|
|
1217
|
-
check.fieldExpr.obj.kind === "var";
|
|
1218
|
-
// Determine which branch is Some (unwrapped) vs None
|
|
1219
|
-
let thenTExpr = e.then;
|
|
1220
|
-
let guardTExpr = guard;
|
|
1221
|
-
// For field chains: replace field access with bound var in TExpr before lowering
|
|
1222
|
-
if (isFieldChain) {
|
|
1223
|
-
const fe = check.fieldExpr;
|
|
1224
|
-
const innerTy = fe.ty.kind === "optional" ? fe.ty.inner : fe.ty;
|
|
1225
|
-
const replacements = [{ fieldName: fe.field, newName: bound, fallbackTy: innerTy }];
|
|
1226
|
-
thenTExpr = fixBoundType(replaceFieldInTExpr(thenTExpr, fe.obj.name, replacements), bound);
|
|
1227
|
-
if (guardTExpr) {
|
|
1228
|
-
guardTExpr = fixBoundType(replaceFieldInTExpr(guardTExpr, fe.obj.name, replacements), bound);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
let thenExpr = lowerExpr(thenTExpr, binds);
|
|
1232
|
-
let elseExpr = lowerExpr(e.else, binds);
|
|
1233
|
-
// For simple vars: replace after lowering
|
|
1234
|
-
if (!isFieldChain) {
|
|
1235
|
-
thenExpr = replaceVar(thenExpr, check.varName, { kind: "var", name: bound }, true);
|
|
1236
|
-
}
|
|
1237
|
-
// Optional wrapping: check if either branch is undefined (produces optional result)
|
|
1238
|
-
const isOptionalResult = (e.then.kind === "var" && e.then.name === "undefined") ||
|
|
1239
|
-
(e.else.kind === "var" && e.else.name === "undefined");
|
|
1240
|
-
if (isOptionalResult) {
|
|
1241
|
-
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
1242
|
-
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
1243
|
-
}
|
|
1244
|
-
// Build Some arm body — add guard for && patterns
|
|
1245
|
-
// Note: if the guard has method calls, the impure path desugars the let to a
|
|
1246
|
-
// statement-level if+match in transformStmt, so this expression-level path only
|
|
1247
|
-
// runs for pure guards. Use null for binds to avoid escaping the match scope.
|
|
1248
|
-
let someBody;
|
|
1249
|
-
if (guard) {
|
|
1250
|
-
let guardExpr = lowerExpr(guardTExpr, null);
|
|
1251
|
-
if (!isFieldChain) {
|
|
1252
|
-
guardExpr = replaceVar(guardExpr, check.varName, { kind: "var", name: bound }, true);
|
|
1253
|
-
}
|
|
1254
|
-
// Guard-else gets the same expression as None arm
|
|
1255
|
-
let guardElse = lowerExpr(e.else, null);
|
|
1256
|
-
if (isOptionalResult) {
|
|
1257
|
-
guardElse = wrapOptionalBranch(guardElse, e.else);
|
|
1258
|
-
}
|
|
1259
|
-
someBody = { kind: "if", cond: guardExpr, then: thenExpr, else: guardElse };
|
|
1260
|
-
}
|
|
1261
|
-
else {
|
|
1262
|
-
someBody = thenExpr;
|
|
1263
|
-
}
|
|
1264
|
-
// Build scrutinee
|
|
1265
|
-
const scrutinee = isFieldChain
|
|
1266
|
-
? lowerExpr(check.fieldExpr, binds)
|
|
1267
|
-
: check.varName;
|
|
1268
|
-
return {
|
|
1269
|
-
kind: "match", scrutinee,
|
|
1270
|
-
arms: [
|
|
1271
|
-
{ pattern: `.some ${bound}`, body: someBody },
|
|
1272
|
-
{ pattern: ".none", body: elseExpr },
|
|
1273
|
-
],
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
1415
|
// ── Pure function generation ─────────────────────────────────
|
|
1277
1416
|
function transformPureBody(stmts, typeDecls) {
|
|
1278
|
-
//
|
|
1279
|
-
if (stmts.length > 0 && stmts[0].kind === "
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1417
|
+
// tagMatch (from narrow's discriminant detection) is the leading stmt and consumes the rest.
|
|
1418
|
+
if (stmts.length > 0 && stmts[0].kind === "tagMatch") {
|
|
1419
|
+
const t = stmts[0];
|
|
1420
|
+
const varName = t.scrutinee.kind === "var" ? t.scrutinee.name : "?";
|
|
1421
|
+
const chain = { varName, typeName: t.typeName, cases: t.cases, fallthrough: t.fallthrough };
|
|
1422
|
+
return transformPureMatch(chain, typeDecls);
|
|
1283
1423
|
}
|
|
1284
1424
|
for (let i = 0; i < stmts.length; i++) {
|
|
1285
1425
|
const s = stmts[i];
|
|
@@ -1293,52 +1433,8 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1293
1433
|
return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
|
|
1294
1434
|
}
|
|
1295
1435
|
case "if": {
|
|
1296
|
-
// Restructure && with optional check: split into nested ifs so
|
|
1297
|
-
// prepareOptionalMatch can detect the optional check and bind the unwrapped value
|
|
1298
|
-
if (s.cond.kind === "binop" && s.cond.op === "&&" && s.else.length === 0) {
|
|
1299
|
-
const extracted = extractLeftmostOptional(s.cond);
|
|
1300
|
-
if (extracted) {
|
|
1301
|
-
const innerIf = { kind: "if", cond: extracted.rest, then: s.then, else: [] };
|
|
1302
|
-
const outerIf = { kind: "if", cond: extracted.optCond, then: [innerIf], else: [] };
|
|
1303
|
-
return transformPureBody([outerIf, ...rest], typeDecls);
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
// Optional narrowing: if (x === undefined) → match x { None => ..., Some(x_val) => ... }
|
|
1307
|
-
const optMatch = prepareOptionalMatch(s, rest);
|
|
1308
|
-
if (optMatch) {
|
|
1309
|
-
// For field chains (a.dueDate !== undefined), replace in TStmt before transform
|
|
1310
|
-
let someBranch = [...optMatch.someBranch, ...rest];
|
|
1311
|
-
const fe = optMatch.check.fieldExpr;
|
|
1312
|
-
if (fe && fe.kind === "field" && fe.obj.kind === "var") {
|
|
1313
|
-
const innerTy = fe.ty.kind === "optional" ? fe.ty.inner : fe.ty;
|
|
1314
|
-
someBranch = replaceFieldsInTStmts(someBranch, fe.obj.name, [
|
|
1315
|
-
{ fieldName: fe.field, newName: optMatch.bound, fallbackTy: innerTy },
|
|
1316
|
-
]);
|
|
1317
|
-
// The replacement keeps the original optional type, but the match binding
|
|
1318
|
-
// unwraps it. Fix the type so downstream record coercion re-wraps with Some().
|
|
1319
|
-
someBranch = someBranch.map(s => mapTStmt(s, e => e.kind === "var" && e.name === optMatch.bound && e.ty.kind === "optional"
|
|
1320
|
-
? { ...e, ty: e.ty.inner } : null));
|
|
1321
|
-
}
|
|
1322
|
-
const someExpr = transformPureBody(someBranch, typeDecls);
|
|
1323
|
-
if (!someExpr)
|
|
1324
|
-
return null;
|
|
1325
|
-
const noneExpr = transformPureBody(optMatch.noneBranch, typeDecls);
|
|
1326
|
-
if (!noneExpr)
|
|
1327
|
-
return null;
|
|
1328
|
-
// For simple vars, replace in Expr after transform
|
|
1329
|
-
const someReplaced = optMatch.check.fieldExpr
|
|
1330
|
-
? someExpr
|
|
1331
|
-
: replaceVar(someExpr, optMatch.check.varName, { kind: "var", name: optMatch.bound }, true);
|
|
1332
|
-
return {
|
|
1333
|
-
kind: "match", scrutinee: optMatch.check.varName,
|
|
1334
|
-
arms: [
|
|
1335
|
-
{ pattern: `.some ${optMatch.bound}`, body: someReplaced },
|
|
1336
|
-
{ pattern: ".none", body: noneExpr },
|
|
1337
|
-
],
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1340
1436
|
// Append rest to both branches so nested ifs that fall through
|
|
1341
|
-
// can reach the continuation (e.g. early return inside then-branch)
|
|
1437
|
+
// can reach the continuation (e.g. early return inside then-branch).
|
|
1342
1438
|
const thenExpr = transformPureBody([...s.then, ...rest], typeDecls);
|
|
1343
1439
|
if (!thenExpr)
|
|
1344
1440
|
return null;
|
|
@@ -1349,6 +1445,27 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1349
1445
|
return { kind: "if", cond: transformExpr(s.cond), then: thenExpr, else: elseExpr };
|
|
1350
1446
|
}
|
|
1351
1447
|
case "switch": return transformPureSwitch(s, typeDecls);
|
|
1448
|
+
case "someMatch": {
|
|
1449
|
+
const path = asTAccessPath(s.scrutinee);
|
|
1450
|
+
if (path) {
|
|
1451
|
+
const replaced = replacePathInTStmts(s.someBody, path, s.binder, s.binderTy);
|
|
1452
|
+
const someExpr = transformPureBody([...replaced, ...rest], typeDecls);
|
|
1453
|
+
if (!someExpr)
|
|
1454
|
+
return null;
|
|
1455
|
+
const noneExpr = transformPureBody([...s.noneBody, ...rest], typeDecls);
|
|
1456
|
+
if (!noneExpr)
|
|
1457
|
+
return null;
|
|
1458
|
+
const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
|
|
1459
|
+
return {
|
|
1460
|
+
kind: "match", scrutinee,
|
|
1461
|
+
arms: [
|
|
1462
|
+
{ pattern: `.some ${s.binder}`, body: someExpr },
|
|
1463
|
+
{ pattern: ".none", body: noneExpr },
|
|
1464
|
+
],
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
throw new Error(`someMatch pure-body scrutinee must be a pure access path, got ${s.scrutinee.kind}`);
|
|
1468
|
+
}
|
|
1352
1469
|
default: return null;
|
|
1353
1470
|
}
|
|
1354
1471
|
}
|
|
@@ -1382,12 +1499,20 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
1382
1499
|
}
|
|
1383
1500
|
function transformPureMatch(chain, typeDecls) {
|
|
1384
1501
|
const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
|
|
1502
|
+
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
1503
|
+
// Synth array-unions have single-field variants and user code refers to the
|
|
1504
|
+
// scrutinee by its bare name, not field-accessed. See emitMatchStmt for
|
|
1505
|
+
// the statement-level counterpart of this substitution.
|
|
1506
|
+
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
1385
1507
|
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => {
|
|
1386
1508
|
let result = transformPureBody(body, typeDecls);
|
|
1387
1509
|
if (!result)
|
|
1388
1510
|
return null;
|
|
1389
1511
|
if (fields.length > 0 && vn)
|
|
1390
1512
|
result = replaceFieldAccess(result, vn, fields);
|
|
1513
|
+
if (isSynthArrayUnion && fields.length === 1 && vn) {
|
|
1514
|
+
result = replaceVarInExpr(result, vn, matchBinder(fields[0].name, vn));
|
|
1515
|
+
}
|
|
1391
1516
|
return result;
|
|
1392
1517
|
});
|
|
1393
1518
|
if (!arms)
|
|
@@ -1395,13 +1520,27 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
1395
1520
|
// Idiomatic TS often has an unreachable fallthrough after exhaustive if-chains on
|
|
1396
1521
|
// discriminated unions. Skip the catch-all arm when all variants are matched,
|
|
1397
1522
|
// since Lean errors on redundant match arms.
|
|
1398
|
-
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
1399
1523
|
const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
|
|
1400
1524
|
if (chain.fallthrough.length > 0 && !allCovered) {
|
|
1401
|
-
const
|
|
1402
|
-
if (
|
|
1403
|
-
|
|
1404
|
-
|
|
1525
|
+
const remaining = remainingVariant(chain.typeName, chain.cases, typeDecls);
|
|
1526
|
+
if (remaining) {
|
|
1527
|
+
// Exactly one variant left — destructure for variant-specific field access.
|
|
1528
|
+
let body = transformPureBody(chain.fallthrough, typeDecls);
|
|
1529
|
+
if (!body)
|
|
1530
|
+
return null;
|
|
1531
|
+
if (remaining.fields.length > 0)
|
|
1532
|
+
body = replaceFieldAccess(body, chain.varName, remaining.fields);
|
|
1533
|
+
if (isSynthArrayUnion && remaining.fields.length === 1) {
|
|
1534
|
+
body = replaceVarInExpr(body, chain.varName, matchBinder(remaining.fields[0].name, chain.varName));
|
|
1535
|
+
}
|
|
1536
|
+
arms.push({ pattern: buildMatchPattern(remaining.name, remaining.fields, chain.varName), body });
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
const body = transformPureBody(chain.fallthrough, typeDecls);
|
|
1540
|
+
if (!body)
|
|
1541
|
+
return null;
|
|
1542
|
+
arms.push({ pattern: "_", body });
|
|
1543
|
+
}
|
|
1405
1544
|
}
|
|
1406
1545
|
return { kind: "match", scrutinee: chain.varName, arms };
|
|
1407
1546
|
}
|
|
@@ -1552,9 +1691,9 @@ export function transformModule(mod, specImport) {
|
|
|
1552
1691
|
continue;
|
|
1553
1692
|
const body = transformPureBody(fn.body, mod.typeDecls);
|
|
1554
1693
|
if (body) {
|
|
1555
|
-
// For
|
|
1694
|
+
// For pure-function lemmas, replace \result with the function call.
|
|
1556
1695
|
const fnCall = { kind: "app", fn: fn.name, args: fn.params.map(p => ({ kind: "var", name: p.name })) };
|
|
1557
|
-
const ensures = fn.ensures.map(e => replaceVar(transformExpr(e), "
|
|
1696
|
+
const ensures = fn.ensures.map(e => replaceVar(transformExpr(e), "\\result", fnCall));
|
|
1558
1697
|
pureDefs.push({
|
|
1559
1698
|
kind: "def",
|
|
1560
1699
|
name: fn.name,
|
|
@@ -1585,18 +1724,35 @@ export function transformModule(mod, specImport) {
|
|
|
1585
1724
|
}
|
|
1586
1725
|
}
|
|
1587
1726
|
const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
|
|
1727
|
+
// Externs: emit as top-of-file `function {:axiom}` (Dafny) declarations.
|
|
1728
|
+
// Any `requires`/`ensures` from the source declaration come along so callers
|
|
1729
|
+
// see the same spec the source itself verified. Substitute `\result` with the
|
|
1730
|
+
// function call (same pattern as for in-file pure-function ensures).
|
|
1731
|
+
const externDecls = (mod.externs ?? []).map(ext => {
|
|
1732
|
+
const fnCall = { kind: "app", fn: ext.flat, args: ext.params.map(p => ({ kind: "var", name: p.name })) };
|
|
1733
|
+
return {
|
|
1734
|
+
kind: "extern",
|
|
1735
|
+
name: ext.flat,
|
|
1736
|
+
typeParams: ext.typeParams,
|
|
1737
|
+
params: ext.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1738
|
+
returnType: ext.returnTy,
|
|
1739
|
+
requires: ext.requires.map(transformExpr),
|
|
1740
|
+
ensures: ext.ensures.map(e => replaceVar(transformExpr(e), "\\result", fnCall)),
|
|
1741
|
+
};
|
|
1742
|
+
});
|
|
1588
1743
|
// Types file
|
|
1589
1744
|
const typesImports = ["LemmaScript"];
|
|
1590
1745
|
let typesFile = null;
|
|
1591
1746
|
const pureNamespace = pureDefs.length > 0
|
|
1592
1747
|
? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
|
|
1593
1748
|
: [];
|
|
1594
|
-
if (typeDecls.length > 0 || pureDefs.length > 0) {
|
|
1749
|
+
if (typeDecls.length > 0 || pureDefs.length > 0 || externDecls.length > 0) {
|
|
1595
1750
|
typesFile = {
|
|
1596
1751
|
comment: " Generated by lsc — Lean types and pure function mirrors.",
|
|
1597
1752
|
imports: typesImports,
|
|
1598
1753
|
options: [],
|
|
1599
|
-
|
|
1754
|
+
// Externs come first so they're in scope for every later declaration.
|
|
1755
|
+
decls: [...externDecls, ...typeDecls, ...pureNamespace],
|
|
1600
1756
|
};
|
|
1601
1757
|
}
|
|
1602
1758
|
// Def file: Velvet methods
|