lemmascript 0.3.3 → 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 +4 -2
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +31 -14
- package/tools/dist/dafny-emit.js +17 -5
- package/tools/dist/extract.js +32 -20
- package/tools/dist/lean-emit.js +53 -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 +313 -220
- package/tools/dist/specparser.js +12 -2
- package/tools/dist/transform.js +217 -419
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) };
|
|
@@ -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 = {
|
|
@@ -186,18 +199,26 @@ function transformExpr(e) { return lowerExpr(e, null); }
|
|
|
186
199
|
/** Wrap an expression in Some/None for optional-typed conditionals.
|
|
187
200
|
* If the raw TExpr is `undefined`, emit `.none`; otherwise wrap in `Some`. */
|
|
188
201
|
function wrapOptionalBranch(expr, raw) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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] };
|
|
192
211
|
}
|
|
193
212
|
function lowerExpr(e, binds) {
|
|
194
|
-
// Monadic lifting: extract embedded method calls to let-binds
|
|
195
|
-
//
|
|
196
|
-
|
|
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") {
|
|
197
219
|
const name = `_t${_liftCounter++}`;
|
|
198
|
-
const fn = e.fn.kind === "var" ? e.fn.name : `${lowerExpr(e.fn, binds)}`;
|
|
199
220
|
const args = e.args.map(a => lowerExpr(a, binds));
|
|
200
|
-
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 } });
|
|
201
222
|
return { kind: "var", name };
|
|
202
223
|
}
|
|
203
224
|
switch (e.kind) {
|
|
@@ -366,7 +387,11 @@ function lowerExpr(e, binds) {
|
|
|
366
387
|
case "index": {
|
|
367
388
|
const idx = transformExpr(e.idx);
|
|
368
389
|
if (e.obj.ty.kind === "map") {
|
|
369
|
-
|
|
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 };
|
|
370
395
|
}
|
|
371
396
|
const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
|
|
372
397
|
return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
|
|
@@ -408,6 +433,10 @@ function lowerExpr(e, binds) {
|
|
|
408
433
|
return { kind: "toNat", expr: lowered };
|
|
409
434
|
return lowered;
|
|
410
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
|
+
}
|
|
411
440
|
// Spec-context map get: result type is non-optional → direct access
|
|
412
441
|
if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
|
|
413
442
|
method = "getDirect";
|
|
@@ -525,65 +554,6 @@ function lowerExpr(e, binds) {
|
|
|
525
554
|
case "exists":
|
|
526
555
|
return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
527
556
|
case "conditional": {
|
|
528
|
-
// Phase 0: Complex expression check (call results, etc.) — resolve substituted
|
|
529
|
-
// and set narrowedVar + narrowedExpr because transform can't detect these.
|
|
530
|
-
if (e.narrowedVar && e.narrowedExpr) {
|
|
531
|
-
const scrutinee = lowerExpr(e.narrowedExpr, binds);
|
|
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
557
|
const cond = lowerExpr(e.cond, binds);
|
|
588
558
|
let thenExpr = lowerExpr(e.then, binds);
|
|
589
559
|
let elseExpr = lowerExpr(e.else, binds);
|
|
@@ -593,6 +563,12 @@ function lowerExpr(e, binds) {
|
|
|
593
563
|
}
|
|
594
564
|
return { kind: "if", cond, then: thenExpr, else: elseExpr };
|
|
595
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`);
|
|
596
572
|
case "havoc":
|
|
597
573
|
// Dafny's * only works in var/assign positions — lift to own declaration
|
|
598
574
|
if (binds) {
|
|
@@ -601,6 +577,41 @@ function lowerExpr(e, binds) {
|
|
|
601
577
|
return { kind: "var", name };
|
|
602
578
|
}
|
|
603
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`);
|
|
604
615
|
}
|
|
605
616
|
}
|
|
606
617
|
function flattenImpl(e) {
|
|
@@ -660,27 +671,6 @@ function transformStmts(stmts, typeDecls) {
|
|
|
660
671
|
let i = 0;
|
|
661
672
|
while (i < stmts.length) {
|
|
662
673
|
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
674
|
// Transform for-of → for-in over range
|
|
685
675
|
if (s.kind === "forof") {
|
|
686
676
|
const varName = s.names[0];
|
|
@@ -844,21 +834,6 @@ function transformStmt(s, typeDecls) {
|
|
|
844
834
|
return stmts;
|
|
845
835
|
}
|
|
846
836
|
}
|
|
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
837
|
const { binds, expr } = liftMethodCalls(s.init);
|
|
863
838
|
return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
|
|
864
839
|
}
|
|
@@ -889,15 +864,19 @@ function transformStmt(s, typeDecls) {
|
|
|
889
864
|
const { binds, expr } = liftMethodCalls(s.expr);
|
|
890
865
|
return [...binds, { kind: "assign", target: receiver, value: expr }];
|
|
891
866
|
}
|
|
892
|
-
// Optional chaining on map.get: m.get(k)?.push(v)
|
|
893
|
-
if
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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;
|
|
898
877
|
const mapName = mapExpr.kind === "var" ? mapExpr.name : undefined;
|
|
899
|
-
const keyExpr = lowerExpr(s.expr.
|
|
900
|
-
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);
|
|
901
880
|
if (mapName) {
|
|
902
881
|
const mapVar = { kind: "var", name: mapName };
|
|
903
882
|
const directGet = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "getDirect", args: [keyExpr], monadic: false };
|
|
@@ -911,18 +890,7 @@ function transformStmt(s, typeDecls) {
|
|
|
911
890
|
return [...binds, { kind: "assign", target: "_", value: expr }];
|
|
912
891
|
}
|
|
913
892
|
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)
|
|
893
|
+
// Lift from condition only (Lean rule: don't lift from branches).
|
|
926
894
|
const { binds, expr: cond } = liftMethodCalls(s.cond);
|
|
927
895
|
return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
928
896
|
}
|
|
@@ -947,180 +915,34 @@ function transformStmt(s, typeDecls) {
|
|
|
947
915
|
return [{ kind: "ghostAssign", target: s.target, value: transformExpr(s.value) }];
|
|
948
916
|
case "assert":
|
|
949
917
|
return [{ kind: "assert", expr: transformExpr(s.expr) }];
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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;
|
|
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)];
|
|
985
939
|
}
|
|
986
940
|
}
|
|
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
941
|
}
|
|
1038
942
|
/** Apply an expression transform to all expressions in a statement (convenience wrapper). */
|
|
1039
943
|
function mapStmtExprs(s, r) {
|
|
1040
944
|
return mapStmt(s, e => r(e));
|
|
1041
945
|
}
|
|
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
946
|
/** Build match arms from variant cases — shared by imperative and pure paths.
|
|
1125
947
|
* Looks up variant fields from typeDecls, builds patterns via buildMatchPattern,
|
|
1126
948
|
* and delegates body transformation to the caller-provided function.
|
|
@@ -1142,10 +964,32 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
|
1142
964
|
function emitMatchStmt(chain, typeDecls) {
|
|
1143
965
|
const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
|
|
1144
966
|
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
|
|
1145
|
-
if (chain.fallthrough.length > 0)
|
|
1146
|
-
|
|
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
|
+
}
|
|
1147
980
|
return { kind: "match", scrutinee: chain.varName, arms };
|
|
1148
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
|
+
}
|
|
1149
993
|
function emitSwitchStmt(s, typeDecls) {
|
|
1150
994
|
const varName = s.expr.kind === "var" ? s.expr.name : "?";
|
|
1151
995
|
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
|
|
@@ -1155,10 +999,7 @@ function emitSwitchStmt(s, typeDecls) {
|
|
|
1155
999
|
arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
|
|
1156
1000
|
return { kind: "match", scrutinee: varName, arms };
|
|
1157
1001
|
}
|
|
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.
|
|
1002
|
+
/** Replace obj.field → replacement var in typed IR.
|
|
1162
1003
|
* Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
|
|
1163
1004
|
function replaceFieldsInTStmts(stmts, objName, replacements) {
|
|
1164
1005
|
if (replacements.length === 0)
|
|
@@ -1199,6 +1040,40 @@ function replaceFieldInTExpr(expr, objName, replacements) {
|
|
|
1199
1040
|
return null;
|
|
1200
1041
|
});
|
|
1201
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
|
+
}
|
|
1202
1077
|
/** Unwrap optional type on match-bound variables in TExpr.
|
|
1203
1078
|
* After replaceFieldInTExpr, the replaced variable carries the original optional
|
|
1204
1079
|
* type from the field declaration. The match binding unwraps it to the inner type. */
|
|
@@ -1206,80 +1081,14 @@ function fixBoundType(expr, boundName) {
|
|
|
1206
1081
|
return mapTExpr(expr, e => e.kind === "var" && e.name === boundName && e.ty.kind === "optional"
|
|
1207
1082
|
? { ...e, ty: e.ty.inner } : null);
|
|
1208
1083
|
}
|
|
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
1084
|
// ── Pure function generation ─────────────────────────────────
|
|
1277
1085
|
function transformPureBody(stmts, typeDecls) {
|
|
1278
|
-
//
|
|
1279
|
-
if (stmts.length > 0 && stmts[0].kind === "
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
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);
|
|
1283
1092
|
}
|
|
1284
1093
|
for (let i = 0; i < stmts.length; i++) {
|
|
1285
1094
|
const s = stmts[i];
|
|
@@ -1293,52 +1102,8 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1293
1102
|
return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
|
|
1294
1103
|
}
|
|
1295
1104
|
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
1105
|
// Append rest to both branches so nested ifs that fall through
|
|
1341
|
-
// can reach the continuation (e.g. early return inside then-branch)
|
|
1106
|
+
// can reach the continuation (e.g. early return inside then-branch).
|
|
1342
1107
|
const thenExpr = transformPureBody([...s.then, ...rest], typeDecls);
|
|
1343
1108
|
if (!thenExpr)
|
|
1344
1109
|
return null;
|
|
@@ -1349,6 +1114,27 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1349
1114
|
return { kind: "if", cond: transformExpr(s.cond), then: thenExpr, else: elseExpr };
|
|
1350
1115
|
}
|
|
1351
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);
|
|
1122
|
+
if (!someExpr)
|
|
1123
|
+
return null;
|
|
1124
|
+
const noneExpr = transformPureBody([...s.noneBody, ...rest], typeDecls);
|
|
1125
|
+
if (!noneExpr)
|
|
1126
|
+
return null;
|
|
1127
|
+
const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
|
|
1128
|
+
return {
|
|
1129
|
+
kind: "match", scrutinee,
|
|
1130
|
+
arms: [
|
|
1131
|
+
{ pattern: `.some ${s.binder}`, body: someExpr },
|
|
1132
|
+
{ pattern: ".none", body: noneExpr },
|
|
1133
|
+
],
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
throw new Error(`someMatch pure-body scrutinee must be a pure access path, got ${s.scrutinee.kind}`);
|
|
1137
|
+
}
|
|
1352
1138
|
default: return null;
|
|
1353
1139
|
}
|
|
1354
1140
|
}
|
|
@@ -1398,10 +1184,22 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
1398
1184
|
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
1399
1185
|
const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
|
|
1400
1186
|
if (chain.fallthrough.length > 0 && !allCovered) {
|
|
1401
|
-
const
|
|
1402
|
-
if (
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
+
}
|
|
1405
1203
|
}
|
|
1406
1204
|
return { kind: "match", scrutinee: chain.varName, arms };
|
|
1407
1205
|
}
|