lemmascript 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/package.json +3 -2
- package/tools/dist/dafny-commands.js +31 -14
- package/tools/dist/dafny-emit.js +63 -17
- package/tools/dist/extract.js +130 -21
- package/tools/dist/lean-emit.js +55 -3
- package/tools/dist/lsc.js +13 -3
- package/tools/dist/narrow.js +737 -0
- package/tools/dist/peephole.js +448 -0
- package/tools/dist/resolve.js +370 -194
- package/tools/dist/specparser.js +12 -2
- package/tools/dist/transform.js +337 -318
package/tools/dist/transform.js
CHANGED
|
@@ -21,10 +21,10 @@ 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 "constructor": return e.args ? { ...e, args: e.args.map(r) } : e;
|
|
28
28
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
29
29
|
case "unop": return { ...e, expr: r(e.expr) };
|
|
30
30
|
case "implies": return { ...e, premises: e.premises.map(r), conclusion: r(e.conclusion) };
|
|
@@ -58,7 +58,10 @@ function mapStmt(s, f) {
|
|
|
58
58
|
case "break":
|
|
59
59
|
case "continue": return s;
|
|
60
60
|
case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapStmt(t, f)), else: s.else.map(t => mapStmt(t, f)) };
|
|
61
|
-
case "match":
|
|
61
|
+
case "match": {
|
|
62
|
+
const scr = typeof s.scrutinee === "string" ? s.scrutinee : r(s.scrutinee);
|
|
63
|
+
return { ...s, scrutinee: scr, arms: s.arms.map(a => ({ ...a, body: a.body.map(t => mapStmt(t, f)) })) };
|
|
64
|
+
}
|
|
62
65
|
case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
|
|
63
66
|
case "forin": return { ...s, bound: r(s.bound), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
|
|
64
67
|
case "ghostLet": return { ...s, value: r(s.value) };
|
|
@@ -87,6 +90,15 @@ function mapTExpr(e, f) {
|
|
|
87
90
|
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
88
91
|
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
89
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 };
|
|
90
102
|
case "forall": return { ...e, body: r(e.body) };
|
|
91
103
|
case "exists": return { ...e, body: r(e.body) };
|
|
92
104
|
case "lambda": return e;
|
|
@@ -110,6 +122,10 @@ function mapTStmt(s, f) {
|
|
|
110
122
|
case "ghostLet": return { ...s, init: r(s.init) };
|
|
111
123
|
case "ghostAssign": return { ...s, value: r(s.value) };
|
|
112
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)) };
|
|
113
129
|
}
|
|
114
130
|
}
|
|
115
131
|
export const LEAN_OPTIONS = {
|
|
@@ -183,18 +199,26 @@ function transformExpr(e) { return lowerExpr(e, null); }
|
|
|
183
199
|
/** Wrap an expression in Some/None for optional-typed conditionals.
|
|
184
200
|
* If the raw TExpr is `undefined`, emit `.none`; otherwise wrap in `Some`. */
|
|
185
201
|
function wrapOptionalBranch(expr, raw) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
202
|
+
// Set type: "Option" so Lean emits `Option.some`/`Option.none` (qualified).
|
|
203
|
+
// The dotted form `.some`/`.none` would be ambiguous in expression positions
|
|
204
|
+
// like the scrutinee of an outer match. Dafny treats `Option.Some` and bare
|
|
205
|
+
// `Some` equivalently — the qualification is harmless there.
|
|
206
|
+
if (raw.kind === "var" && raw.name === "undefined")
|
|
207
|
+
return { kind: "constructor", name: "none", type: "Option" };
|
|
208
|
+
if (raw.ty.kind === "optional")
|
|
209
|
+
return expr; // already Option<T>, don't double-wrap
|
|
210
|
+
return { kind: "constructor", name: "some", type: "Option", args: [expr] };
|
|
189
211
|
}
|
|
190
212
|
function lowerExpr(e, binds) {
|
|
191
|
-
// Monadic lifting: extract embedded method calls to let-binds
|
|
192
|
-
//
|
|
193
|
-
|
|
213
|
+
// Monadic lifting: extract embedded method calls to let-binds.
|
|
214
|
+
// `callKind: "method"` means a global var-fn call (classifyCall returns
|
|
215
|
+
// "method" only for `fn.kind === "var"`). Receiver method calls have
|
|
216
|
+
// callKind "unknown" and fall through to the regular case below where
|
|
217
|
+
// they become `methodCall`.
|
|
218
|
+
if (binds && e.kind === "call" && e.callKind === "method" && e.fn.kind === "var") {
|
|
194
219
|
const name = `_t${_liftCounter++}`;
|
|
195
|
-
const fn = e.fn.kind === "var" ? e.fn.name : `${lowerExpr(e.fn, binds)}`;
|
|
196
220
|
const args = e.args.map(a => lowerExpr(a, binds));
|
|
197
|
-
binds.push({ kind: "let-bind", name, value: { kind: "app", fn, args } });
|
|
221
|
+
binds.push({ kind: "let-bind", name, value: { kind: "app", fn: e.fn.name, args } });
|
|
198
222
|
return { kind: "var", name };
|
|
199
223
|
}
|
|
200
224
|
switch (e.kind) {
|
|
@@ -347,11 +371,27 @@ function lowerExpr(e, binds) {
|
|
|
347
371
|
return { kind: "field", obj: transformExpr(e.obj), field: "length" };
|
|
348
372
|
if (e.field === "size" && (e.obj.ty.kind === "map" || e.obj.ty.kind === "set"))
|
|
349
373
|
return { kind: "field", obj: transformExpr(e.obj), field: "collectionSize" };
|
|
374
|
+
// Boolean discriminant bare access: `result.ok` where Result has variants
|
|
375
|
+
// {ok: true, ...} | {ok: false, ...}. String discriminants are always used via
|
|
376
|
+
// comparison (x.kind === 'Foo' → x.Foo?), but boolean discriminants are used
|
|
377
|
+
// as bare truthiness checks. Emit as the Dafny discriminator predicate for the
|
|
378
|
+
// 'true' variant: result.ok → result.true_?
|
|
379
|
+
if (e.isDiscriminant && e.obj.ty.kind === "user") {
|
|
380
|
+
const baseName = e.obj.ty.name.includes("<") ? e.obj.ty.name.slice(0, e.obj.ty.name.indexOf("<")) : e.obj.ty.name;
|
|
381
|
+
const decl = _typeDecls.find(d => d.name === baseName && d.kind === "discriminated-union");
|
|
382
|
+
if (decl?.variants?.some(v => v.name === "true")) {
|
|
383
|
+
return { kind: "field", obj: transformExpr(e.obj), field: "true_?" };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
350
386
|
return { kind: "field", obj: transformExpr(e.obj), field: e.field };
|
|
351
387
|
case "index": {
|
|
352
388
|
const idx = transformExpr(e.idx);
|
|
353
389
|
if (e.obj.ty.kind === "map") {
|
|
354
|
-
|
|
390
|
+
// Mirrors the .get() → .getDirect switch at line ~453: when resolve has
|
|
391
|
+
// narrowed the index type to non-optional (via `k in m` atoms in scope),
|
|
392
|
+
// emit direct access; otherwise keep the Option-producing `get`.
|
|
393
|
+
const method = e.ty.kind !== "optional" ? "getDirect" : "get";
|
|
394
|
+
return { kind: "methodCall", obj: transformExpr(e.obj), objTy: e.obj.ty, method, args: [idx], monadic: false };
|
|
355
395
|
}
|
|
356
396
|
const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
|
|
357
397
|
return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
|
|
@@ -393,10 +433,23 @@ function lowerExpr(e, binds) {
|
|
|
393
433
|
return { kind: "toNat", expr: lowered };
|
|
394
434
|
return lowered;
|
|
395
435
|
});
|
|
436
|
+
// arr.concat(otherArr): array argument → real concatenation, not push
|
|
437
|
+
if (method === "concat" && e.fn.obj.ty.kind === "array" && e.args.length === 1 && e.args[0].ty.kind === "array") {
|
|
438
|
+
return { kind: "binop", op: "arrayConcat", left: recv, right: args[0] };
|
|
439
|
+
}
|
|
396
440
|
// Spec-context map get: result type is non-optional → direct access
|
|
397
441
|
if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
|
|
398
442
|
method = "getDirect";
|
|
399
443
|
}
|
|
444
|
+
// map.set(k, v): if v is an Optional-wrapped map get, unwrap to getDirect
|
|
445
|
+
// (the desugared spread { ...m, [k]: m2[k] } becomes m.set(k, m2.get(k)),
|
|
446
|
+
// but the value should be direct access, not Optional)
|
|
447
|
+
if (method === "set" && e.fn.obj.ty.kind === "map" && args.length === 2) {
|
|
448
|
+
const val = args[1];
|
|
449
|
+
if (val.kind === "methodCall" && val.method === "get" && val.objTy.kind === "map") {
|
|
450
|
+
args[1] = { ...val, method: "getDirect" };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
400
453
|
// Check if any lambda arg has monadic body
|
|
401
454
|
const needsMonadic = _opts.monadic && args.some(a => a.kind === "lambda" && isMonadicBody(a.body));
|
|
402
455
|
const result = { kind: "methodCall", obj: recv, objTy: e.fn.obj.ty, method, args, monadic: needsMonadic };
|
|
@@ -479,6 +532,10 @@ function lowerExpr(e, binds) {
|
|
|
479
532
|
});
|
|
480
533
|
return { kind: "record", spread: lowerExpr(e.spread, binds), fields: loweredFields };
|
|
481
534
|
}
|
|
535
|
+
// Empty record with map type → empty map
|
|
536
|
+
if (e.fields.length === 0 && !e.spread && e.ty.kind === "map") {
|
|
537
|
+
return { kind: "emptyMap" };
|
|
538
|
+
}
|
|
482
539
|
return { kind: "record", spread: null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
|
|
483
540
|
}
|
|
484
541
|
case "arrayLiteral":
|
|
@@ -497,56 +554,21 @@ function lowerExpr(e, binds) {
|
|
|
497
554
|
case "exists":
|
|
498
555
|
return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
499
556
|
case "conditional": {
|
|
500
|
-
|
|
501
|
-
const condBinds = (e.narrowedVar && e.narrowedExpr) ? null : binds;
|
|
502
|
-
const cond = lowerExpr(e.cond, condBinds);
|
|
557
|
+
const cond = lowerExpr(e.cond, binds);
|
|
503
558
|
let thenExpr = lowerExpr(e.then, binds);
|
|
504
559
|
let elseExpr = lowerExpr(e.else, binds);
|
|
505
|
-
// Explicit !== undefined with narrowedExpr → match Some/None on the optional expression
|
|
506
|
-
if (e.narrowedVar && e.narrowedExpr) {
|
|
507
|
-
const scrutinee = lowerExpr(e.narrowedExpr, binds);
|
|
508
|
-
const bound = matchBinder(e.narrowedVar);
|
|
509
|
-
if (bound !== e.narrowedVar) {
|
|
510
|
-
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
511
|
-
}
|
|
512
|
-
// Wrap in Some/None only when result is optional (one branch is undefined)
|
|
513
|
-
if (e.ty.kind === "optional") {
|
|
514
|
-
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
515
|
-
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
516
|
-
}
|
|
517
|
-
return {
|
|
518
|
-
kind: "match", scrutinee,
|
|
519
|
-
arms: [
|
|
520
|
-
{ pattern: `.some ${bound}`, body: thenExpr },
|
|
521
|
-
{ pattern: ".none", body: elseExpr },
|
|
522
|
-
],
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
// Optional cond with narrowedVar → match Some/None (truthiness)
|
|
526
|
-
if (e.narrowedVar && e.cond.ty.kind === "optional") {
|
|
527
|
-
const bound = matchBinder(e.narrowedVar);
|
|
528
|
-
// Replace the synthetic/narrowed var with the match-bound name
|
|
529
|
-
if (bound !== e.narrowedVar) {
|
|
530
|
-
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
531
|
-
}
|
|
532
|
-
// The match produces an Optional: wrap branches in Some/None.
|
|
533
|
-
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
534
|
-
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
535
|
-
return {
|
|
536
|
-
kind: "match", scrutinee: cond,
|
|
537
|
-
arms: [
|
|
538
|
-
{ pattern: `.some ${bound}`, body: thenExpr },
|
|
539
|
-
{ pattern: ".none", body: elseExpr },
|
|
540
|
-
],
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
// Non-optional: regular if with optional wrapping
|
|
544
560
|
if (e.ty.kind === "optional") {
|
|
545
561
|
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
546
562
|
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
547
563
|
}
|
|
548
564
|
return { kind: "if", cond, then: thenExpr, else: elseExpr };
|
|
549
565
|
}
|
|
566
|
+
case "optChain":
|
|
567
|
+
// Narrow should have rewritten optChain to someMatch.
|
|
568
|
+
throw new Error(`optChain reached transform — narrow should have rewritten it`);
|
|
569
|
+
case "nullish":
|
|
570
|
+
// Narrow should have rewritten nullish to someMatch.
|
|
571
|
+
throw new Error(`nullish reached transform — narrow should have rewritten it`);
|
|
550
572
|
case "havoc":
|
|
551
573
|
// Dafny's * only works in var/assign positions — lift to own declaration
|
|
552
574
|
if (binds) {
|
|
@@ -555,6 +577,41 @@ function lowerExpr(e, binds) {
|
|
|
555
577
|
return { kind: "var", name };
|
|
556
578
|
}
|
|
557
579
|
return { kind: "havoc", type: e.ty };
|
|
580
|
+
case "someMatch": {
|
|
581
|
+
let someBody;
|
|
582
|
+
let scrutinee;
|
|
583
|
+
const path = asTAccessPath(e.scrutinee);
|
|
584
|
+
if (path) {
|
|
585
|
+
// Pure access path (var or any depth of obj.f.g.h) — substitute the
|
|
586
|
+
// path with the binder pre-lowering.
|
|
587
|
+
const replaced = replacePathInTExpr(e.someBody, path, e.binder, e.binderTy);
|
|
588
|
+
someBody = lowerExpr(replaced, binds);
|
|
589
|
+
scrutinee = path.fields.length === 0 ? path.rootVar : lowerExpr(e.scrutinee, binds);
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
// Complex scrutinee — narrow pre-bound the someBody to use the binder directly,
|
|
593
|
+
// so no substitution needed. Used by optChain rewrites.
|
|
594
|
+
someBody = lowerExpr(e.someBody, binds);
|
|
595
|
+
scrutinee = lowerExpr(e.scrutinee, binds);
|
|
596
|
+
}
|
|
597
|
+
let noneBody = lowerExpr(e.noneBody, binds);
|
|
598
|
+
if (e.ty.kind === "optional") {
|
|
599
|
+
someBody = wrapOptionalBranch(someBody, e.someBody);
|
|
600
|
+
noneBody = wrapOptionalBranch(noneBody, e.noneBody);
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
kind: "match", scrutinee,
|
|
604
|
+
arms: [
|
|
605
|
+
{ pattern: `.some ${e.binder}`, body: someBody },
|
|
606
|
+
{ pattern: ".none", body: noneBody },
|
|
607
|
+
],
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
case "tagMatch":
|
|
611
|
+
// Spec/expr-position tagMatch — narrow shouldn't produce these (it only
|
|
612
|
+
// emits stmt-form tagMatch from if-chain detection on stmts). If reached,
|
|
613
|
+
// bug in narrow.
|
|
614
|
+
throw new Error(`tagMatch reached lowerExpr — narrow only emits stmt-form tagMatch`);
|
|
558
615
|
}
|
|
559
616
|
}
|
|
560
617
|
function flattenImpl(e) {
|
|
@@ -614,27 +671,6 @@ function transformStmts(stmts, typeDecls) {
|
|
|
614
671
|
let i = 0;
|
|
615
672
|
while (i < stmts.length) {
|
|
616
673
|
const s = stmts[i];
|
|
617
|
-
// Detect discriminant if-chain → match
|
|
618
|
-
if (s.kind === "if") {
|
|
619
|
-
const chain = detectDiscriminantChain(stmts.slice(i));
|
|
620
|
-
if (chain) {
|
|
621
|
-
result.push(emitMatchStmt(chain.chain, typeDecls));
|
|
622
|
-
i += chain.consumed;
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
// Detect optional check → match on Some/None
|
|
626
|
-
const optMatch = prepareOptionalMatch(s, stmts.slice(i + 1));
|
|
627
|
-
if (optMatch) {
|
|
628
|
-
result.push(emitOptionalMatch(optMatch.check.varName, optMatch.check.negated, s, typeDecls, stmts.slice(i + 1), optMatch.check.fieldExpr));
|
|
629
|
-
// If rest was consumed into the Some branch, skip remaining
|
|
630
|
-
const origSome = optMatch.check.negated ? s.else : s.then;
|
|
631
|
-
if (origSome.length === 0 && i + 1 < stmts.length) {
|
|
632
|
-
return result;
|
|
633
|
-
}
|
|
634
|
-
i++;
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
674
|
// Transform for-of → for-in over range
|
|
639
675
|
if (s.kind === "forof") {
|
|
640
676
|
const varName = s.names[0];
|
|
@@ -644,13 +680,13 @@ function transformStmts(stmts, typeDecls) {
|
|
|
644
680
|
if (s.names.length === 1 && s.iterable.ty.kind === "map") {
|
|
645
681
|
const keyName = s.names[0];
|
|
646
682
|
const keyTy = s.nameTypes[0] ?? s.iterable.ty.key ?? { kind: "unknown" };
|
|
647
|
-
const keysSeqName = `_${keyName}_keys`;
|
|
648
|
-
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
649
|
-
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
650
|
-
const keysVar = { kind: "var", name: keysSeqName };
|
|
651
683
|
const count = _forofCounters.get(keyName) ?? 0;
|
|
652
684
|
_forofCounters.set(keyName, count + 1);
|
|
653
685
|
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
686
|
+
const keysSeqName = `_${keyName}_keys${suffix}`;
|
|
687
|
+
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
688
|
+
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
689
|
+
const keysVar = { kind: "var", name: keysSeqName };
|
|
654
690
|
const idxName = `_${keyName}_idx${suffix}`;
|
|
655
691
|
const idx = { kind: "var", name: idxName };
|
|
656
692
|
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
@@ -670,13 +706,13 @@ function transformStmts(stmts, typeDecls) {
|
|
|
670
706
|
const keyName = s.names[0], valueName = s.names[1];
|
|
671
707
|
const keyTy = s.nameTypes[0] ?? { kind: "unknown" };
|
|
672
708
|
const valueTy = s.nameTypes[1] ?? { kind: "unknown" };
|
|
673
|
-
const keysSeqName = `_${keyName}_keys`;
|
|
674
|
-
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
675
|
-
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
676
|
-
const keysVar = { kind: "var", name: keysSeqName };
|
|
677
709
|
const count = _forofCounters.get(keyName) ?? 0;
|
|
678
710
|
_forofCounters.set(keyName, count + 1);
|
|
679
711
|
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
712
|
+
const keysSeqName = `_${keyName}_keys${suffix}`;
|
|
713
|
+
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
714
|
+
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
715
|
+
const keysVar = { kind: "var", name: keysSeqName };
|
|
680
716
|
const idxName = `_${keyName}_idx${suffix}`;
|
|
681
717
|
const idx = { kind: "var", name: idxName };
|
|
682
718
|
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
@@ -828,15 +864,19 @@ function transformStmt(s, typeDecls) {
|
|
|
828
864
|
const { binds, expr } = liftMethodCalls(s.expr);
|
|
829
865
|
return [...binds, { kind: "assign", target: receiver, value: expr }];
|
|
830
866
|
}
|
|
831
|
-
// Optional chaining on map.get: m.get(k)?.push(v)
|
|
832
|
-
if
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
867
|
+
// Optional chaining on map.get at statement level: m.get(k)?.push(v)
|
|
868
|
+
// → if k in m { m[k] := m[k] + [v] } (actual mutation, not value-discard).
|
|
869
|
+
// Narrow rewrote this to a someMatch — destructure to find the underlying
|
|
870
|
+
// m.get(k) scrutinee and the .push(v) body call.
|
|
871
|
+
if (s.expr.kind === "someMatch" &&
|
|
872
|
+
s.expr.scrutinee.kind === "call" && s.expr.scrutinee.fn.kind === "field" &&
|
|
873
|
+
s.expr.scrutinee.fn.field === "get" && s.expr.scrutinee.fn.obj.ty.kind === "map" &&
|
|
874
|
+
s.expr.someBody.kind === "call" && s.expr.someBody.fn.kind === "field" &&
|
|
875
|
+
s.expr.someBody.fn.field === "push") {
|
|
876
|
+
const mapExpr = s.expr.scrutinee.fn.obj;
|
|
837
877
|
const mapName = mapExpr.kind === "var" ? mapExpr.name : undefined;
|
|
838
|
-
const keyExpr = lowerExpr(s.expr.
|
|
839
|
-
const pushArg = lowerExpr(s.expr.args[0], null);
|
|
878
|
+
const keyExpr = lowerExpr(s.expr.scrutinee.args[0], null);
|
|
879
|
+
const pushArg = lowerExpr(s.expr.someBody.args[0], null);
|
|
840
880
|
if (mapName) {
|
|
841
881
|
const mapVar = { kind: "var", name: mapName };
|
|
842
882
|
const directGet = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "getDirect", args: [keyExpr], monadic: false };
|
|
@@ -850,18 +890,7 @@ function transformStmt(s, typeDecls) {
|
|
|
850
890
|
return [...binds, { kind: "assign", target: "_", value: expr }];
|
|
851
891
|
}
|
|
852
892
|
case "if": {
|
|
853
|
-
//
|
|
854
|
-
// from a && chain and nest the rest inside. Handles left-associative chains:
|
|
855
|
-
// if ((x !== undefined && b) && c) → if (x !== undefined) { if (b && c) { ... } }
|
|
856
|
-
if (s.cond.kind === "binop" && s.cond.op === "&&" && s.else.length === 0) {
|
|
857
|
-
const extracted = extractLeftmostOptional(s.cond);
|
|
858
|
-
if (extracted) {
|
|
859
|
-
const innerIf = { kind: "if", cond: extracted.rest, then: s.then, else: [] };
|
|
860
|
-
const outerIf = { kind: "if", cond: extracted.optCond, then: [innerIf], else: [] };
|
|
861
|
-
return transformStmts([outerIf], typeDecls);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
// Lift from condition only (Lean rule: don't lift from branches)
|
|
893
|
+
// Lift from condition only (Lean rule: don't lift from branches).
|
|
865
894
|
const { binds, expr: cond } = liftMethodCalls(s.cond);
|
|
866
895
|
return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
867
896
|
}
|
|
@@ -886,178 +915,34 @@ function transformStmt(s, typeDecls) {
|
|
|
886
915
|
return [{ kind: "ghostAssign", target: s.target, value: transformExpr(s.value) }];
|
|
887
916
|
case "assert":
|
|
888
917
|
return [{ kind: "assert", expr: transformExpr(s.expr) }];
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
// Walk consecutive top-level ifs on the same discriminant
|
|
911
|
-
let consumed = 0;
|
|
912
|
-
for (let i = 0; i < stmts.length; i++) {
|
|
913
|
-
const s = stmts[i];
|
|
914
|
-
if (s.kind !== "if")
|
|
915
|
-
break;
|
|
916
|
-
const p = parseDiscriminantCond(s.cond);
|
|
917
|
-
if (!p || p.varName !== first.varName)
|
|
918
|
-
break;
|
|
919
|
-
cases.push({ variant: p.variant, body: s.then });
|
|
920
|
-
consumed = i + 1;
|
|
921
|
-
if (s.else.length > 0) {
|
|
922
|
-
const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
|
|
923
|
-
return cases.length > 0 ? { chain: { ...first, cases, fallthrough: ft }, consumed } : null;
|
|
918
|
+
case "someMatch": {
|
|
919
|
+
const path = asTAccessPath(s.scrutinee);
|
|
920
|
+
if (path) {
|
|
921
|
+
const replaced = replacePathInTStmts(s.someBody, path, s.binder, s.binderTy);
|
|
922
|
+
const someBody = transformStmts(replaced, typeDecls);
|
|
923
|
+
const noneBody = transformStmts(s.noneBody, typeDecls);
|
|
924
|
+
const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
|
|
925
|
+
return [{
|
|
926
|
+
kind: "match", scrutinee,
|
|
927
|
+
arms: [
|
|
928
|
+
{ pattern: `.some ${s.binder}`, body: someBody },
|
|
929
|
+
{ pattern: ".none", body: noneBody },
|
|
930
|
+
],
|
|
931
|
+
}];
|
|
932
|
+
}
|
|
933
|
+
throw new Error(`someMatch stmt scrutinee must be a pure access path, got ${s.scrutinee.kind}`);
|
|
934
|
+
}
|
|
935
|
+
case "tagMatch": {
|
|
936
|
+
const varName = s.scrutinee.kind === "var" ? s.scrutinee.name : "?";
|
|
937
|
+
const chain = { varName, typeName: s.typeName, cases: s.cases, fallthrough: s.fallthrough };
|
|
938
|
+
return [emitMatchStmt(chain, typeDecls)];
|
|
924
939
|
}
|
|
925
940
|
}
|
|
926
|
-
if (cases.length === 0)
|
|
927
|
-
return null;
|
|
928
|
-
return { chain: { ...first, cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
|
|
929
|
-
}
|
|
930
|
-
function parseDiscriminantCond(cond) {
|
|
931
|
-
// Pattern: x.discriminant === "variant"
|
|
932
|
-
if (cond.kind !== "binop" || cond.op !== "===" || cond.right.kind !== "str")
|
|
933
|
-
return null;
|
|
934
|
-
if (cond.left.kind !== "field" || !cond.left.isDiscriminant)
|
|
935
|
-
return null;
|
|
936
|
-
if (cond.left.obj.kind !== "var" || cond.left.obj.ty.kind !== "user")
|
|
937
|
-
return null;
|
|
938
|
-
return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
939
|
-
}
|
|
940
|
-
function emitOptionalMatch(varName, negated, s, typeDecls, restStmts, fieldExpr) {
|
|
941
|
-
let someBranch = negated ? s.else : s.then;
|
|
942
|
-
const noneBranch = negated ? s.then : s.else;
|
|
943
|
-
// Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
|
|
944
|
-
// so include remaining statements as the Some body
|
|
945
|
-
if (someBranch.length === 0 && restStmts && restStmts.length > 0) {
|
|
946
|
-
someBranch = restStmts;
|
|
947
|
-
}
|
|
948
|
-
const bound = matchBinder(`${varName}_val`);
|
|
949
|
-
// Replace the narrowed variable/field in the Some branch body.
|
|
950
|
-
// Field chains: replace in TStmt before transform (so downstream narrowing sees simple vars).
|
|
951
|
-
// Simple vars: replace in IR after transform (the original mechanism).
|
|
952
|
-
let someBody;
|
|
953
|
-
if (fieldExpr && fieldExpr.kind === "field" && fieldExpr.obj.kind === "var") {
|
|
954
|
-
const innerTy = fieldExpr.ty.kind === "optional" ? fieldExpr.ty.inner : fieldExpr.ty;
|
|
955
|
-
const replaced = replaceFieldsInTStmts(someBranch, fieldExpr.obj.name, [
|
|
956
|
-
{ fieldName: fieldExpr.field, newName: bound, fallbackTy: innerTy },
|
|
957
|
-
]);
|
|
958
|
-
someBody = transformStmts(replaced, typeDecls);
|
|
959
|
-
}
|
|
960
|
-
else {
|
|
961
|
-
const transformed = transformStmts(someBranch, typeDecls);
|
|
962
|
-
someBody = transformed.map(stmt => mapStmtExprs(stmt, e => replaceVar(e, varName, { kind: "var", name: bound })));
|
|
963
|
-
}
|
|
964
|
-
return {
|
|
965
|
-
kind: "match", scrutinee: varName,
|
|
966
|
-
arms: [
|
|
967
|
-
{ pattern: `.some ${bound}`, body: someBody },
|
|
968
|
-
{ pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
|
|
969
|
-
],
|
|
970
|
-
};
|
|
971
941
|
}
|
|
972
942
|
/** Apply an expression transform to all expressions in a statement (convenience wrapper). */
|
|
973
943
|
function mapStmtExprs(s, r) {
|
|
974
944
|
return mapStmt(s, e => r(e));
|
|
975
945
|
}
|
|
976
|
-
// ── Optional narrowing helpers ──────────────────────────────
|
|
977
|
-
//
|
|
978
|
-
// Optional narrowing converts TS `if (x === undefined)` patterns to Dafny
|
|
979
|
-
// `match x { Some(val) => ..., None => ... }`.
|
|
980
|
-
//
|
|
981
|
-
// The resolve phase (resolve.ts) handles:
|
|
982
|
-
// - Flow narrowing: after `if (x === undefined) return`, x is non-optional
|
|
983
|
-
// - && narrowing: in `x !== undefined && f(x)`, f(x) sees x as non-optional
|
|
984
|
-
// - Conditional narrowing: in `x !== undefined ? x.field : default`, sets
|
|
985
|
-
// narrowedVar/narrowedExpr on TExpr for the transform phase
|
|
986
|
-
//
|
|
987
|
-
// The transform phase (here) handles:
|
|
988
|
-
// - Statement-level: `transformStmts` detects optional checks → `emitOptionalMatch`
|
|
989
|
-
// - Expression-level: `lowerExpr` conditional reads narrowedVar/narrowedExpr → match
|
|
990
|
-
// - && restructuring: `extractLeftmostOptional` splits `&&` chains into nested ifs
|
|
991
|
-
// so `emitOptionalMatch` can detect the inner optional check
|
|
992
|
-
//
|
|
993
|
-
// Both phases detect `v !== undefined` patterns. The resolve phase uses
|
|
994
|
-
// `detectOptionalCheck` (on RawExpr), the transform uses `parseOptionalCheck` (on TExpr).
|
|
995
|
-
// These are separate because they operate on different IR types, but both handle
|
|
996
|
-
// simple variables and field access chains.
|
|
997
|
-
/** Shared logic for optional match in both imperative and pure function paths.
|
|
998
|
-
* Detects optional check, selects branches, handles early-return consumption.
|
|
999
|
-
* Returns null if the condition is not an optional check. */
|
|
1000
|
-
function prepareOptionalMatch(s, restStmts) {
|
|
1001
|
-
const check = parseOptionalCheck(s.cond);
|
|
1002
|
-
if (!check)
|
|
1003
|
-
return null;
|
|
1004
|
-
let someBranch = check.negated ? s.else : s.then;
|
|
1005
|
-
const noneBranch = check.negated ? s.then : (s.else.length > 0 ? s.else : restStmts);
|
|
1006
|
-
// Early-return pattern: Some branch is empty → consume rest of block
|
|
1007
|
-
if (someBranch.length === 0 && restStmts.length > 0)
|
|
1008
|
-
someBranch = restStmts;
|
|
1009
|
-
const bound = matchBinder(`${check.varName}_val`);
|
|
1010
|
-
return { check, someBranch, noneBranch, bound };
|
|
1011
|
-
}
|
|
1012
|
-
/** Extract the leftmost optional check from a && chain, returning the check and the rest.
|
|
1013
|
-
* (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
|
|
1014
|
-
function extractLeftmostOptional(cond) {
|
|
1015
|
-
if (cond.kind !== "binop" || cond.op !== "&&")
|
|
1016
|
-
return null;
|
|
1017
|
-
const check = parseOptionalCheck(cond.left);
|
|
1018
|
-
if (check && !check.negated)
|
|
1019
|
-
return { optCond: cond.left, rest: cond.right };
|
|
1020
|
-
if (cond.left.kind === "binop" && cond.left.op === "&&") {
|
|
1021
|
-
const inner = extractLeftmostOptional(cond.left);
|
|
1022
|
-
if (inner)
|
|
1023
|
-
return { optCond: inner.optCond, rest: { ...cond, left: inner.rest } };
|
|
1024
|
-
}
|
|
1025
|
-
return null;
|
|
1026
|
-
}
|
|
1027
|
-
/** Detect `v !== undefined` or `undefined !== v` where v has optional type.
|
|
1028
|
-
* Also handles field access chains like `obj.field !== undefined`.
|
|
1029
|
-
* When `fieldExpr` is returned, callers must use field-aware replacement. */
|
|
1030
|
-
function parseOptionalCheck(cond) {
|
|
1031
|
-
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
|
|
1032
|
-
return null;
|
|
1033
|
-
let varExpr = null;
|
|
1034
|
-
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
1035
|
-
varExpr = cond.left;
|
|
1036
|
-
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
1037
|
-
varExpr = cond.right;
|
|
1038
|
-
if (!varExpr)
|
|
1039
|
-
return null;
|
|
1040
|
-
if (varExpr.kind === "var" && varExpr.ty.kind === "optional") {
|
|
1041
|
-
return { varName: varExpr.name, negated: cond.op === "===" };
|
|
1042
|
-
}
|
|
1043
|
-
if (varExpr.kind === "field" && varExpr.ty.kind === "optional") {
|
|
1044
|
-
// Serialize field chain as a dotted name for use as match scrutinee
|
|
1045
|
-
const chain = serializeFieldChain(varExpr);
|
|
1046
|
-
if (chain)
|
|
1047
|
-
return { varName: chain, negated: cond.op === "===", fieldExpr: varExpr };
|
|
1048
|
-
}
|
|
1049
|
-
return null;
|
|
1050
|
-
}
|
|
1051
|
-
/** Serialize a field access chain to a dotted variable path, or null if not a simple chain. */
|
|
1052
|
-
function serializeFieldChain(e) {
|
|
1053
|
-
if (e.kind === "var")
|
|
1054
|
-
return e.name;
|
|
1055
|
-
if (e.kind === "field") {
|
|
1056
|
-
const parent = serializeFieldChain(e.obj);
|
|
1057
|
-
return parent ? `${parent}.${e.field}` : null;
|
|
1058
|
-
}
|
|
1059
|
-
return null;
|
|
1060
|
-
}
|
|
1061
946
|
/** Build match arms from variant cases — shared by imperative and pure paths.
|
|
1062
947
|
* Looks up variant fields from typeDecls, builds patterns via buildMatchPattern,
|
|
1063
948
|
* and delegates body transformation to the caller-provided function.
|
|
@@ -1079,10 +964,32 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
|
1079
964
|
function emitMatchStmt(chain, typeDecls) {
|
|
1080
965
|
const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
|
|
1081
966
|
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
|
|
1082
|
-
if (chain.fallthrough.length > 0)
|
|
1083
|
-
|
|
967
|
+
if (chain.fallthrough.length > 0) {
|
|
968
|
+
const remaining = remainingVariant(chain, typeDecls);
|
|
969
|
+
if (remaining) {
|
|
970
|
+
// Exactly one variant left — destructure so the fallthrough body can
|
|
971
|
+
// access variant-specific fields (Lean requires this; Dafny tolerates `_`).
|
|
972
|
+
const pattern = buildMatchPattern(remaining.name, remaining.fields, chain.varName);
|
|
973
|
+
const body = transformStmts(replaceFieldAccessInTStmts(chain.fallthrough, chain.varName, remaining.fields), typeDecls);
|
|
974
|
+
arms.push({ pattern, body });
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
|
|
978
|
+
}
|
|
979
|
+
}
|
|
1084
980
|
return { kind: "match", scrutinee: chain.varName, arms };
|
|
1085
981
|
}
|
|
982
|
+
/** If the chain has matched all variants but one, return that remaining variant. */
|
|
983
|
+
function remainingVariant(chain, typeDecls) {
|
|
984
|
+
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
985
|
+
if (!decl?.variants)
|
|
986
|
+
return null;
|
|
987
|
+
const matched = new Set(chain.cases.map(c => c.variant));
|
|
988
|
+
const remaining = decl.variants.filter(v => !matched.has(v.name));
|
|
989
|
+
if (remaining.length !== 1)
|
|
990
|
+
return null;
|
|
991
|
+
return remaining[0];
|
|
992
|
+
}
|
|
1086
993
|
function emitSwitchStmt(s, typeDecls) {
|
|
1087
994
|
const varName = s.expr.kind === "var" ? s.expr.name : "?";
|
|
1088
995
|
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
|
|
@@ -1092,10 +999,7 @@ function emitSwitchStmt(s, typeDecls) {
|
|
|
1092
999
|
arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
|
|
1093
1000
|
return { kind: "match", scrutinee: varName, arms };
|
|
1094
1001
|
}
|
|
1095
|
-
/** Replace obj.field → replacement var in typed IR
|
|
1096
|
-
* Used by discriminant match/switch and optional match to rewrite field accesses
|
|
1097
|
-
* into simple variables before the transform phase, so downstream narrowing
|
|
1098
|
-
* (parseOptionalCheck, extractLeftmostOptional) sees simple variable references.
|
|
1002
|
+
/** Replace obj.field → replacement var in typed IR.
|
|
1099
1003
|
* Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
|
|
1100
1004
|
function replaceFieldsInTStmts(stmts, objName, replacements) {
|
|
1101
1005
|
if (replacements.length === 0)
|
|
@@ -1120,13 +1024,71 @@ function replaceFieldAccessInTStmts(stmts, varName, fields) {
|
|
|
1120
1024
|
fallbackTy: f.type ?? parseTsType(f.tsType),
|
|
1121
1025
|
})));
|
|
1122
1026
|
}
|
|
1027
|
+
/** Replace obj.field → replacement var in typed IR expressions (before lowering).
|
|
1028
|
+
* Mirrors replaceFieldsInTStmts but operates on a single TExpr tree. */
|
|
1029
|
+
function replaceFieldInTExpr(expr, objName, replacements) {
|
|
1030
|
+
if (replacements.length === 0)
|
|
1031
|
+
return expr;
|
|
1032
|
+
return mapTExpr(expr, e => {
|
|
1033
|
+
if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === objName) {
|
|
1034
|
+
const r = replacements.find(r => r.fieldName === e.field);
|
|
1035
|
+
if (r) {
|
|
1036
|
+
const ty = e.ty.kind !== "unknown" ? e.ty : r.fallbackTy;
|
|
1037
|
+
return { kind: "var", name: r.newName, ty };
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return null;
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
function asTAccessPath(e) {
|
|
1044
|
+
if (e.kind === "var")
|
|
1045
|
+
return { rootVar: e.name, fields: [] };
|
|
1046
|
+
if (e.kind === "field") {
|
|
1047
|
+
const inner = asTAccessPath(e.obj);
|
|
1048
|
+
if (!inner)
|
|
1049
|
+
return null;
|
|
1050
|
+
return { rootVar: inner.rootVar, fields: [...inner.fields, e.field] };
|
|
1051
|
+
}
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
/** Does TExpr `e` match the given access path exactly? */
|
|
1055
|
+
function matchesAccessPath(e, path) {
|
|
1056
|
+
const collected = [];
|
|
1057
|
+
let cur = e;
|
|
1058
|
+
while (cur.kind === "field") {
|
|
1059
|
+
collected.unshift(cur.field);
|
|
1060
|
+
cur = cur.obj;
|
|
1061
|
+
}
|
|
1062
|
+
if (cur.kind !== "var" || cur.name !== path.rootVar)
|
|
1063
|
+
return false;
|
|
1064
|
+
if (collected.length !== path.fields.length)
|
|
1065
|
+
return false;
|
|
1066
|
+
return collected.every((f, i) => f === path.fields[i]);
|
|
1067
|
+
}
|
|
1068
|
+
/** Replace every TExpr matching `path` with `var(binder, binderTy)`. */
|
|
1069
|
+
function replacePathInTExpr(expr, path, binder, binderTy) {
|
|
1070
|
+
return mapTExpr(expr, e => matchesAccessPath(e, path)
|
|
1071
|
+
? { kind: "var", name: binder, ty: binderTy } : null);
|
|
1072
|
+
}
|
|
1073
|
+
function replacePathInTStmts(stmts, path, binder, binderTy) {
|
|
1074
|
+
return stmts.map(s => mapTStmt(s, e => matchesAccessPath(e, path)
|
|
1075
|
+
? { kind: "var", name: binder, ty: binderTy } : null));
|
|
1076
|
+
}
|
|
1077
|
+
/** Unwrap optional type on match-bound variables in TExpr.
|
|
1078
|
+
* After replaceFieldInTExpr, the replaced variable carries the original optional
|
|
1079
|
+
* type from the field declaration. The match binding unwraps it to the inner type. */
|
|
1080
|
+
function fixBoundType(expr, boundName) {
|
|
1081
|
+
return mapTExpr(expr, e => e.kind === "var" && e.name === boundName && e.ty.kind === "optional"
|
|
1082
|
+
? { ...e, ty: e.ty.inner } : null);
|
|
1083
|
+
}
|
|
1123
1084
|
// ── Pure function generation ─────────────────────────────────
|
|
1124
1085
|
function transformPureBody(stmts, typeDecls) {
|
|
1125
|
-
//
|
|
1126
|
-
if (stmts.length > 0 && stmts[0].kind === "
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1086
|
+
// tagMatch (from narrow's discriminant detection) is the leading stmt and consumes the rest.
|
|
1087
|
+
if (stmts.length > 0 && stmts[0].kind === "tagMatch") {
|
|
1088
|
+
const t = stmts[0];
|
|
1089
|
+
const varName = t.scrutinee.kind === "var" ? t.scrutinee.name : "?";
|
|
1090
|
+
const chain = { varName, typeName: t.typeName, cases: t.cases, fallthrough: t.fallthrough };
|
|
1091
|
+
return transformPureMatch(chain, typeDecls);
|
|
1130
1092
|
}
|
|
1131
1093
|
for (let i = 0; i < stmts.length; i++) {
|
|
1132
1094
|
const s = stmts[i];
|
|
@@ -1140,34 +1102,39 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1140
1102
|
return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
|
|
1141
1103
|
}
|
|
1142
1104
|
case "if": {
|
|
1143
|
-
//
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1105
|
+
// Append rest to both branches so nested ifs that fall through
|
|
1106
|
+
// can reach the continuation (e.g. early return inside then-branch).
|
|
1107
|
+
const thenExpr = transformPureBody([...s.then, ...rest], typeDecls);
|
|
1108
|
+
if (!thenExpr)
|
|
1109
|
+
return null;
|
|
1110
|
+
const elseStmts = s.else.length > 0 ? [...s.else, ...rest] : rest;
|
|
1111
|
+
const elseExpr = transformPureBody(elseStmts, typeDecls);
|
|
1112
|
+
if (!elseExpr)
|
|
1113
|
+
return null;
|
|
1114
|
+
return { kind: "if", cond: transformExpr(s.cond), then: thenExpr, else: elseExpr };
|
|
1115
|
+
}
|
|
1116
|
+
case "switch": return transformPureSwitch(s, typeDecls);
|
|
1117
|
+
case "someMatch": {
|
|
1118
|
+
const path = asTAccessPath(s.scrutinee);
|
|
1119
|
+
if (path) {
|
|
1120
|
+
const replaced = replacePathInTStmts(s.someBody, path, s.binder, s.binderTy);
|
|
1121
|
+
const someExpr = transformPureBody([...replaced, ...rest], typeDecls);
|
|
1147
1122
|
if (!someExpr)
|
|
1148
1123
|
return null;
|
|
1149
|
-
const noneExpr = transformPureBody(
|
|
1124
|
+
const noneExpr = transformPureBody([...s.noneBody, ...rest], typeDecls);
|
|
1150
1125
|
if (!noneExpr)
|
|
1151
1126
|
return null;
|
|
1152
|
-
const
|
|
1127
|
+
const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
|
|
1153
1128
|
return {
|
|
1154
|
-
kind: "match", scrutinee
|
|
1129
|
+
kind: "match", scrutinee,
|
|
1155
1130
|
arms: [
|
|
1156
|
-
{ pattern: `.some ${
|
|
1131
|
+
{ pattern: `.some ${s.binder}`, body: someExpr },
|
|
1157
1132
|
{ pattern: ".none", body: noneExpr },
|
|
1158
1133
|
],
|
|
1159
1134
|
};
|
|
1160
1135
|
}
|
|
1161
|
-
|
|
1162
|
-
if (!thenExpr)
|
|
1163
|
-
return null;
|
|
1164
|
-
const elseBranch = s.else.length > 0 ? s.else : rest;
|
|
1165
|
-
const elseExpr = transformPureBody(elseBranch, typeDecls);
|
|
1166
|
-
if (!elseExpr)
|
|
1167
|
-
return null;
|
|
1168
|
-
return { kind: "if", cond: transformExpr(s.cond), then: thenExpr, else: elseExpr };
|
|
1136
|
+
throw new Error(`someMatch pure-body scrutinee must be a pure access path, got ${s.scrutinee.kind}`);
|
|
1169
1137
|
}
|
|
1170
|
-
case "switch": return transformPureSwitch(s, typeDecls);
|
|
1171
1138
|
default: return null;
|
|
1172
1139
|
}
|
|
1173
1140
|
}
|
|
@@ -1217,10 +1184,22 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
1217
1184
|
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
1218
1185
|
const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
|
|
1219
1186
|
if (chain.fallthrough.length > 0 && !allCovered) {
|
|
1220
|
-
const
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1187
|
+
const remaining = remainingVariant(chain, typeDecls);
|
|
1188
|
+
if (remaining) {
|
|
1189
|
+
// Exactly one variant left — destructure for variant-specific field access.
|
|
1190
|
+
let body = transformPureBody(chain.fallthrough, typeDecls);
|
|
1191
|
+
if (!body)
|
|
1192
|
+
return null;
|
|
1193
|
+
if (remaining.fields.length > 0)
|
|
1194
|
+
body = replaceFieldAccess(body, chain.varName, remaining.fields);
|
|
1195
|
+
arms.push({ pattern: buildMatchPattern(remaining.name, remaining.fields, chain.varName), body });
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
const body = transformPureBody(chain.fallthrough, typeDecls);
|
|
1199
|
+
if (!body)
|
|
1200
|
+
return null;
|
|
1201
|
+
arms.push({ pattern: "_", body });
|
|
1202
|
+
}
|
|
1224
1203
|
}
|
|
1225
1204
|
return { kind: "match", scrutinee: chain.varName, arms };
|
|
1226
1205
|
}
|
|
@@ -1293,17 +1272,38 @@ function findReassignedNames(stmts, names) {
|
|
|
1293
1272
|
return found;
|
|
1294
1273
|
}
|
|
1295
1274
|
/** Replace all occurrences of a variable name with a new expression. */
|
|
1296
|
-
|
|
1275
|
+
/**
|
|
1276
|
+
* Replace all occurrences of variable `name` with `replacement`.
|
|
1277
|
+
* If `narrowing` is true, the replacement is an unwrapped Optional value
|
|
1278
|
+
* (e.g., replacing `x: Option<T>` with `x_val: T`). In that case, when the
|
|
1279
|
+
* variable appears directly as a record spread field value, it's wrapped in
|
|
1280
|
+
* Some() to preserve the field's Optional type.
|
|
1281
|
+
*/
|
|
1282
|
+
function replaceVar(e, name, replacement, narrowing) {
|
|
1283
|
+
const rec = (expr) => replaceVar(expr, name, replacement, narrowing);
|
|
1297
1284
|
return mapExpr(e, x => {
|
|
1298
1285
|
if (x.kind === "var" && x.name === name)
|
|
1299
1286
|
return replacement;
|
|
1287
|
+
// Record spread: wrap direct variable uses in field values with Some when narrowing
|
|
1288
|
+
if (narrowing && x.kind === "record" && x.spread) {
|
|
1289
|
+
return {
|
|
1290
|
+
...x,
|
|
1291
|
+
spread: rec(x.spread),
|
|
1292
|
+
fields: x.fields.map(f => {
|
|
1293
|
+
if (f.value.kind === "var" && f.value.name === name) {
|
|
1294
|
+
return { ...f, value: { kind: "app", fn: "Some", args: [replacement] } };
|
|
1295
|
+
}
|
|
1296
|
+
return { ...f, value: rec(f.value) };
|
|
1297
|
+
}),
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
1300
|
// Don't descend past bindings that shadow the name
|
|
1301
1301
|
if (x.kind === "forall" && x.var === name)
|
|
1302
1302
|
return x;
|
|
1303
1303
|
if (x.kind === "exists" && x.var === name)
|
|
1304
1304
|
return x;
|
|
1305
1305
|
if (x.kind === "let" && x.name === name)
|
|
1306
|
-
return { ...x, value: replaceVar(x.value, name, replacement) };
|
|
1306
|
+
return { ...x, value: replaceVar(x.value, name, replacement, narrowing) };
|
|
1307
1307
|
return null;
|
|
1308
1308
|
});
|
|
1309
1309
|
}
|
|
@@ -1344,25 +1344,43 @@ export function transformModule(mod, specImport) {
|
|
|
1344
1344
|
}));
|
|
1345
1345
|
// Pure function mirrors
|
|
1346
1346
|
const pureDefs = [];
|
|
1347
|
+
const defByMethods = [];
|
|
1347
1348
|
for (const fn of mod.functions) {
|
|
1348
1349
|
if (!fn.isPure)
|
|
1349
1350
|
continue;
|
|
1350
1351
|
const body = transformPureBody(fn.body, mod.typeDecls);
|
|
1351
|
-
if (
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1352
|
+
if (body) {
|
|
1353
|
+
// For ensures, replace \result (→ "res") with the function call
|
|
1354
|
+
const fnCall = { kind: "app", fn: fn.name, args: fn.params.map(p => ({ kind: "var", name: p.name })) };
|
|
1355
|
+
const ensures = fn.ensures.map(e => replaceVar(transformExpr(e), "res", fnCall));
|
|
1356
|
+
pureDefs.push({
|
|
1357
|
+
kind: "def",
|
|
1358
|
+
name: fn.name,
|
|
1359
|
+
typeParams: fn.typeParams,
|
|
1360
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1361
|
+
returnType: fn.returnTy,
|
|
1362
|
+
requires: fn.requires.map(transformExpr),
|
|
1363
|
+
ensures,
|
|
1364
|
+
decreases: fn.decreases ? transformExpr(fn.decreases) : null,
|
|
1365
|
+
body,
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
else if (fn.forcePure) {
|
|
1369
|
+
// //@ pure but body can't be auto-converted — emit function by method
|
|
1370
|
+
_forofCounters.clear();
|
|
1371
|
+
const methodBody = transformStmts(fn.body, mod.typeDecls);
|
|
1372
|
+
defByMethods.push({
|
|
1373
|
+
kind: "def-by-method",
|
|
1374
|
+
name: fn.name,
|
|
1375
|
+
typeParams: fn.typeParams,
|
|
1376
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1377
|
+
returnType: fn.returnTy,
|
|
1378
|
+
requires: fn.requires.map(transformExpr),
|
|
1379
|
+
ensures: fn.ensures.map(transformExpr),
|
|
1380
|
+
decreases: fn.decreases ? transformExpr(fn.decreases) : null,
|
|
1381
|
+
methodBody,
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1366
1384
|
}
|
|
1367
1385
|
const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
|
|
1368
1386
|
// Types file
|
|
@@ -1381,7 +1399,8 @@ export function transformModule(mod, specImport) {
|
|
|
1381
1399
|
}
|
|
1382
1400
|
// Def file: Velvet methods
|
|
1383
1401
|
// Pure functions get a thin wrapper that calls Pure.fnName
|
|
1384
|
-
|
|
1402
|
+
// def-by-method functions also skip their method wrappers
|
|
1403
|
+
const pureDefNames = new Set([...pureDefs.map(d => d.name), ...defByMethods.map(d => d.name)]);
|
|
1385
1404
|
const methods = mod.functions.map(fn => {
|
|
1386
1405
|
const ensures = [];
|
|
1387
1406
|
for (const e of fn.ensures) {
|
|
@@ -1448,7 +1467,7 @@ export function transformModule(mod, specImport) {
|
|
|
1448
1467
|
{ key: "loom.semantics.termination", value: '"total"' },
|
|
1449
1468
|
{ key: "loom.semantics.choice", value: '"demonic"' },
|
|
1450
1469
|
],
|
|
1451
|
-
decls: [...constDecls, ...methods, ...classDecls],
|
|
1470
|
+
decls: [...constDecls, ...defByMethods, ...methods, ...classDecls],
|
|
1452
1471
|
};
|
|
1453
1472
|
return { typesFile, defFile };
|
|
1454
1473
|
}
|