lemmascript 0.3.0 → 0.3.2
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/package.json +1 -1
- package/tools/dist/dafny-commands.js +3 -2
- package/tools/dist/dafny-emit.js +111 -155
- package/tools/dist/extract.js +88 -4
- package/tools/dist/lsc.js +7 -1
- package/tools/dist/resolve.js +229 -181
- package/tools/dist/transform.js +245 -147
package/tools/dist/transform.js
CHANGED
|
@@ -66,9 +66,6 @@ function mapStmt(s, f) {
|
|
|
66
66
|
case "assert": return { ...s, expr: r(s.expr) };
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
function mapStmts(stmts, f) {
|
|
70
|
-
return stmts.map(s => mapStmt(s, f));
|
|
71
|
-
}
|
|
72
69
|
/** Map over all sub-expressions in a TExpr (typed IR). */
|
|
73
70
|
function mapTExpr(e, f) {
|
|
74
71
|
const hit = f(e);
|
|
@@ -133,6 +130,12 @@ let _typeDecls = [];
|
|
|
133
130
|
function matchBinder(fieldName, prefix) {
|
|
134
131
|
return prefix ? `_${prefix}_${fieldName}` : `_${fieldName}`;
|
|
135
132
|
}
|
|
133
|
+
/** Build a match arm pattern like `.VariantName _v_field1 _v_field2` from variant info. */
|
|
134
|
+
function buildMatchPattern(variantName, fields, scopePrefix) {
|
|
135
|
+
if (fields.length === 0)
|
|
136
|
+
return `.${variantName}`;
|
|
137
|
+
return `.${variantName} ${fields.map(f => matchBinder(f.name, scopePrefix)).join(" ")}`;
|
|
138
|
+
}
|
|
136
139
|
const _forofCounters = new Map();
|
|
137
140
|
function isNat(ty) { return ty.kind === "nat"; }
|
|
138
141
|
function isArray(ty) { return ty.kind === "array"; }
|
|
@@ -177,6 +180,13 @@ function transformExpr(e) { return lowerExpr(e, null); }
|
|
|
177
180
|
* a method call can appear inline in TS. It does NOT propagate into
|
|
178
181
|
* field, index, record, forall, or exists sub-expressions.
|
|
179
182
|
*/
|
|
183
|
+
/** Wrap an expression in Some/None for optional-typed conditionals.
|
|
184
|
+
* If the raw TExpr is `undefined`, emit `.none`; otherwise wrap in `Some`. */
|
|
185
|
+
function wrapOptionalBranch(expr, raw) {
|
|
186
|
+
return (raw.kind === "var" && raw.name === "undefined")
|
|
187
|
+
? { kind: "constructor", name: ".none" }
|
|
188
|
+
: { kind: "app", fn: "Some", args: [expr] };
|
|
189
|
+
}
|
|
180
190
|
function lowerExpr(e, binds) {
|
|
181
191
|
// Monadic lifting: extract embedded method calls to let-binds
|
|
182
192
|
// Pass binds through to args so nested method calls are also lifted
|
|
@@ -340,6 +350,9 @@ function lowerExpr(e, binds) {
|
|
|
340
350
|
return { kind: "field", obj: transformExpr(e.obj), field: e.field };
|
|
341
351
|
case "index": {
|
|
342
352
|
const idx = transformExpr(e.idx);
|
|
353
|
+
if (e.obj.ty.kind === "map") {
|
|
354
|
+
return { kind: "methodCall", obj: transformExpr(e.obj), objTy: e.obj.ty, method: "get", args: [idx], monadic: false };
|
|
355
|
+
}
|
|
343
356
|
const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
|
|
344
357
|
return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
|
|
345
358
|
}
|
|
@@ -427,23 +440,41 @@ function lowerExpr(e, binds) {
|
|
|
427
440
|
}
|
|
428
441
|
}
|
|
429
442
|
}
|
|
430
|
-
// For spread records,
|
|
443
|
+
// For spread records, propagate declared field types and wrap optionals
|
|
431
444
|
if (e.spread) {
|
|
432
445
|
const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
|
|
433
446
|
const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
|
|
434
447
|
const structDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "record") : undefined;
|
|
448
|
+
// Also check discriminated-union variants for field types
|
|
449
|
+
const unionDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "discriminated-union") : undefined;
|
|
435
450
|
const loweredFields = e.fields.map(f => {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
451
|
+
// Propagate declared field type onto value if it has unknown type
|
|
452
|
+
let fieldValue = f.value;
|
|
453
|
+
const fieldDecl = structDecl?.fields?.find(sf => sf.name === f.name);
|
|
454
|
+
let declaredTy;
|
|
455
|
+
if (fieldDecl) {
|
|
456
|
+
declaredTy = fieldDecl.type;
|
|
457
|
+
}
|
|
458
|
+
else if (unionDecl?.variants) {
|
|
459
|
+
for (const v of unionDecl.variants) {
|
|
460
|
+
const vf = v.fields.find(vf => vf.name === f.name);
|
|
461
|
+
if (vf) {
|
|
462
|
+
declaredTy = vf.type;
|
|
463
|
+
break;
|
|
444
464
|
}
|
|
445
465
|
}
|
|
446
466
|
}
|
|
467
|
+
if (declaredTy && fieldValue.ty.kind === "unknown") {
|
|
468
|
+
fieldValue = { ...fieldValue, ty: declaredTy };
|
|
469
|
+
}
|
|
470
|
+
let value = lowerExpr(fieldValue, binds);
|
|
471
|
+
// Wrap non-optional values in Some for optional fields
|
|
472
|
+
if (declaredTy?.kind === "optional") {
|
|
473
|
+
const isUndef = f.value.kind === "var" && f.value.name === "undefined";
|
|
474
|
+
if (f.value.ty.kind !== "optional" && !isUndef) {
|
|
475
|
+
value = { kind: "app", fn: "Some", args: [value] };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
447
478
|
return { name: f.name, value };
|
|
448
479
|
});
|
|
449
480
|
return { kind: "record", spread: lowerExpr(e.spread, binds), fields: loweredFields };
|
|
@@ -466,7 +497,9 @@ function lowerExpr(e, binds) {
|
|
|
466
497
|
case "exists":
|
|
467
498
|
return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
468
499
|
case "conditional": {
|
|
469
|
-
|
|
500
|
+
// When narrowedExpr is set, the match replaces the condition — don't lift from it
|
|
501
|
+
const condBinds = (e.narrowedVar && e.narrowedExpr) ? null : binds;
|
|
502
|
+
const cond = lowerExpr(e.cond, condBinds);
|
|
470
503
|
let thenExpr = lowerExpr(e.then, binds);
|
|
471
504
|
let elseExpr = lowerExpr(e.else, binds);
|
|
472
505
|
// Explicit !== undefined with narrowedExpr → match Some/None on the optional expression
|
|
@@ -476,11 +509,11 @@ function lowerExpr(e, binds) {
|
|
|
476
509
|
if (bound !== e.narrowedVar) {
|
|
477
510
|
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
478
511
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
}
|
|
484
517
|
return {
|
|
485
518
|
kind: "match", scrutinee,
|
|
486
519
|
arms: [
|
|
@@ -497,12 +530,8 @@ function lowerExpr(e, binds) {
|
|
|
497
530
|
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
498
531
|
}
|
|
499
532
|
// The match produces an Optional: wrap branches in Some/None.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
? { kind: "constructor", name: ".none" }
|
|
503
|
-
: { kind: "app", fn: "Some", args: [expr] };
|
|
504
|
-
thenExpr = wrapSomeNone(thenExpr, e.then);
|
|
505
|
-
elseExpr = wrapSomeNone(elseExpr, e.else);
|
|
533
|
+
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
534
|
+
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
506
535
|
return {
|
|
507
536
|
kind: "match", scrutinee: cond,
|
|
508
537
|
arms: [
|
|
@@ -513,18 +542,8 @@ function lowerExpr(e, binds) {
|
|
|
513
542
|
}
|
|
514
543
|
// Non-optional: regular if with optional wrapping
|
|
515
544
|
if (e.ty.kind === "optional") {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
thenExpr = { kind: "app", fn: "Some", args: [thenExpr] };
|
|
521
|
-
}
|
|
522
|
-
if (e.else.kind === "var" && e.else.name === "undefined") {
|
|
523
|
-
elseExpr = { kind: "constructor", name: ".none" };
|
|
524
|
-
}
|
|
525
|
-
else {
|
|
526
|
-
elseExpr = { kind: "app", fn: "Some", args: [elseExpr] };
|
|
527
|
-
}
|
|
545
|
+
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
546
|
+
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
528
547
|
}
|
|
529
548
|
return { kind: "if", cond, then: thenExpr, else: elseExpr };
|
|
530
549
|
}
|
|
@@ -571,7 +590,7 @@ function ensuresToMatch(e, typeDecls) {
|
|
|
571
590
|
if (!variant)
|
|
572
591
|
return null;
|
|
573
592
|
const fields = variant.fields;
|
|
574
|
-
const pattern =
|
|
593
|
+
const pattern = buildMatchPattern(variantName, fields, obj.name);
|
|
575
594
|
let rhs = transformExpr(e.right);
|
|
576
595
|
rhs = replaceFieldAccess(rhs, obj.name, fields);
|
|
577
596
|
return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
|
|
@@ -604,13 +623,12 @@ function transformStmts(stmts, typeDecls) {
|
|
|
604
623
|
continue;
|
|
605
624
|
}
|
|
606
625
|
// Detect optional check → match on Some/None
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
609
|
-
|
|
610
|
-
result.push(emitOptionalMatch(opt.varName, opt.negated, s, typeDecls, rest));
|
|
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));
|
|
611
629
|
// If rest was consumed into the Some branch, skip remaining
|
|
612
|
-
const
|
|
613
|
-
if (
|
|
630
|
+
const origSome = optMatch.check.negated ? s.else : s.then;
|
|
631
|
+
if (origSome.length === 0 && i + 1 < stmts.length) {
|
|
614
632
|
return result;
|
|
615
633
|
}
|
|
616
634
|
i++;
|
|
@@ -622,6 +640,31 @@ function transformStmts(stmts, typeDecls) {
|
|
|
622
640
|
const varName = s.names[0];
|
|
623
641
|
const varTy = s.nameTypes[0] ?? { kind: "unknown" };
|
|
624
642
|
let iterExpr = transformExpr(s.iterable);
|
|
643
|
+
// Map key-only iteration: for (const k in record) → iterate keys only
|
|
644
|
+
if (s.names.length === 1 && s.iterable.ty.kind === "map") {
|
|
645
|
+
const keyName = s.names[0];
|
|
646
|
+
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
|
+
const count = _forofCounters.get(keyName) ?? 0;
|
|
652
|
+
_forofCounters.set(keyName, count + 1);
|
|
653
|
+
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
654
|
+
const idxName = `_${keyName}_idx${suffix}`;
|
|
655
|
+
const idx = { kind: "var", name: idxName };
|
|
656
|
+
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
657
|
+
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
658
|
+
const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
|
|
659
|
+
const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
|
|
660
|
+
result.push({
|
|
661
|
+
kind: "forin", idx: idxName, bound: arrSize,
|
|
662
|
+
invariants: [boundInv, ...s.invariants.map(transformExpr)],
|
|
663
|
+
body: [letKey, ...bodyStmts],
|
|
664
|
+
});
|
|
665
|
+
i++;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
625
668
|
// Map iteration: for (const [k, v] of map) → iterate keys, look up values
|
|
626
669
|
if (s.names.length >= 2 && s.iterable.ty.kind === "map") {
|
|
627
670
|
const keyName = s.names[0], valueName = s.names[1];
|
|
@@ -894,7 +937,7 @@ function parseDiscriminantCond(cond) {
|
|
|
894
937
|
return null;
|
|
895
938
|
return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
896
939
|
}
|
|
897
|
-
function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
|
|
940
|
+
function emitOptionalMatch(varName, negated, s, typeDecls, restStmts, fieldExpr) {
|
|
898
941
|
let someBranch = negated ? s.else : s.then;
|
|
899
942
|
const noneBranch = negated ? s.then : s.else;
|
|
900
943
|
// Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
|
|
@@ -903,19 +946,69 @@ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
|
|
|
903
946
|
someBranch = restStmts;
|
|
904
947
|
}
|
|
905
948
|
const bound = matchBinder(`${varName}_val`);
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
+
};
|
|
914
971
|
}
|
|
915
972
|
/** Apply an expression transform to all expressions in a statement (convenience wrapper). */
|
|
916
973
|
function mapStmtExprs(s, r) {
|
|
917
974
|
return mapStmt(s, e => r(e));
|
|
918
975
|
}
|
|
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
|
+
}
|
|
919
1012
|
/** Extract the leftmost optional check from a && chain, returning the check and the rest.
|
|
920
1013
|
* (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
|
|
921
1014
|
function extractLeftmostOptional(cond) {
|
|
@@ -931,7 +1024,9 @@ function extractLeftmostOptional(cond) {
|
|
|
931
1024
|
}
|
|
932
1025
|
return null;
|
|
933
1026
|
}
|
|
934
|
-
/** Detect `v !== undefined` or `undefined !== v` where v has optional type.
|
|
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. */
|
|
935
1030
|
function parseOptionalCheck(cond) {
|
|
936
1031
|
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
|
|
937
1032
|
return null;
|
|
@@ -940,21 +1035,50 @@ function parseOptionalCheck(cond) {
|
|
|
940
1035
|
varExpr = cond.left;
|
|
941
1036
|
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
942
1037
|
varExpr = cond.right;
|
|
943
|
-
if (!varExpr
|
|
1038
|
+
if (!varExpr)
|
|
944
1039
|
return null;
|
|
945
|
-
|
|
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;
|
|
946
1050
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
+
/** Build match arms from variant cases — shared by imperative and pure paths.
|
|
1062
|
+
* Looks up variant fields from typeDecls, builds patterns via buildMatchPattern,
|
|
1063
|
+
* and delegates body transformation to the caller-provided function.
|
|
1064
|
+
* Returns null if any body transformation returns null (pure path abort). */
|
|
1065
|
+
function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
1066
|
+
const decl = typeName ? typeDecls.find(d => d.name === typeName) : undefined;
|
|
1067
|
+
const arms = [];
|
|
1068
|
+
for (const c of cases) {
|
|
1069
|
+
const variant = decl?.variants?.find(v => v.name === c.name);
|
|
951
1070
|
const fields = variant?.fields ?? [];
|
|
952
|
-
const pattern =
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
1071
|
+
const pattern = buildMatchPattern(c.name, fields, varName);
|
|
1072
|
+
const body = transformBody(c.body, varName, fields);
|
|
1073
|
+
if (body === null)
|
|
1074
|
+
return null;
|
|
1075
|
+
arms.push({ pattern, body });
|
|
1076
|
+
}
|
|
1077
|
+
return arms;
|
|
1078
|
+
}
|
|
1079
|
+
function emitMatchStmt(chain, typeDecls) {
|
|
1080
|
+
const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
|
|
1081
|
+
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
|
|
958
1082
|
if (chain.fallthrough.length > 0)
|
|
959
1083
|
arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
|
|
960
1084
|
return { kind: "match", scrutinee: chain.varName, arms };
|
|
@@ -962,59 +1086,39 @@ function emitMatchStmt(chain, typeDecls) {
|
|
|
962
1086
|
function emitSwitchStmt(s, typeDecls) {
|
|
963
1087
|
const varName = s.expr.kind === "var" ? s.expr.name : "?";
|
|
964
1088
|
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
|
|
965
|
-
const
|
|
966
|
-
const arms =
|
|
967
|
-
const variant = decl?.variants?.find(v => v.name === c.label);
|
|
968
|
-
const fields = variant?.fields ?? [];
|
|
969
|
-
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name, varName)).join(" ")}` : `.${c.label}`;
|
|
970
|
-
// Replace field accesses in TStmt BEFORE transforming, so optional narrowing sees simple vars
|
|
971
|
-
const replaced = replaceFieldAccessInTStmts(c.body, varName, fields);
|
|
972
|
-
const body = transformStmts(replaced, typeDecls);
|
|
973
|
-
return { pattern, body };
|
|
974
|
-
});
|
|
1089
|
+
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1090
|
+
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
|
|
975
1091
|
if (s.defaultBody.length > 0)
|
|
976
1092
|
arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
|
|
977
1093
|
return { kind: "match", scrutinee: varName, arms };
|
|
978
1094
|
}
|
|
979
|
-
/** Replace obj.field →
|
|
980
|
-
*
|
|
981
|
-
*
|
|
982
|
-
|
|
983
|
-
|
|
1095
|
+
/** Replace obj.field → replacement var in typed IR (before transform).
|
|
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.
|
|
1099
|
+
* Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
|
|
1100
|
+
function replaceFieldsInTStmts(stmts, objName, replacements) {
|
|
1101
|
+
if (replacements.length === 0)
|
|
984
1102
|
return stmts;
|
|
985
1103
|
return stmts.map(s => mapTStmt(s, e => {
|
|
986
|
-
if (e.kind === "field" && e.obj.kind === "var" && e.obj.name ===
|
|
987
|
-
const
|
|
988
|
-
if (
|
|
989
|
-
const ty = e.ty.kind !== "unknown" ? e.ty :
|
|
990
|
-
return { kind: "var", name:
|
|
1104
|
+
if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === objName) {
|
|
1105
|
+
const r = replacements.find(r => r.fieldName === e.field);
|
|
1106
|
+
if (r) {
|
|
1107
|
+
const ty = e.ty.kind !== "unknown" ? e.ty : r.fallbackTy;
|
|
1108
|
+
return { kind: "var", name: r.newName, ty };
|
|
991
1109
|
}
|
|
992
1110
|
}
|
|
993
1111
|
return null;
|
|
994
1112
|
}));
|
|
995
1113
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
}
|
|
1005
|
-
return null;
|
|
1006
|
-
};
|
|
1007
|
-
const result = [];
|
|
1008
|
-
for (const s of stmts) {
|
|
1009
|
-
// If a let shadows the matched variable, stop replacing from here on
|
|
1010
|
-
if (s.kind === "let" && s.name === varName) {
|
|
1011
|
-
result.push(s.value ? { ...s, value: mapExpr(s.value, f) } : s);
|
|
1012
|
-
result.push(...stmts.slice(result.length));
|
|
1013
|
-
break;
|
|
1014
|
-
}
|
|
1015
|
-
result.push(mapStmt(s, f));
|
|
1016
|
-
}
|
|
1017
|
-
return result;
|
|
1114
|
+
/** Replace all variant fields of obj → match binder vars in typed IR.
|
|
1115
|
+
* Thin wrapper around replaceFieldsInTStmts for discriminant match/switch. */
|
|
1116
|
+
function replaceFieldAccessInTStmts(stmts, varName, fields) {
|
|
1117
|
+
return replaceFieldsInTStmts(stmts, varName, fields.map(f => ({
|
|
1118
|
+
fieldName: f.name,
|
|
1119
|
+
newName: matchBinder(f.name, varName),
|
|
1120
|
+
fallbackTy: f.type ?? parseTsType(f.tsType),
|
|
1121
|
+
})));
|
|
1018
1122
|
}
|
|
1019
1123
|
// ── Pure function generation ─────────────────────────────────
|
|
1020
1124
|
function transformPureBody(stmts, typeDecls) {
|
|
@@ -1037,24 +1141,19 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1037
1141
|
}
|
|
1038
1142
|
case "if": {
|
|
1039
1143
|
// Optional narrowing: if (x === undefined) → match x { None => ..., Some(x_val) => ... }
|
|
1040
|
-
const
|
|
1041
|
-
if (
|
|
1042
|
-
|
|
1043
|
-
const noneBranch = optCheck.negated ? s.then : (s.else.length > 0 ? s.else : rest);
|
|
1044
|
-
if (someBranch.length === 0)
|
|
1045
|
-
someBranch = rest;
|
|
1046
|
-
const bound = matchBinder(`${optCheck.varName}_val`);
|
|
1047
|
-
const someExpr = transformPureBody(someBranch, typeDecls);
|
|
1144
|
+
const optMatch = prepareOptionalMatch(s, rest);
|
|
1145
|
+
if (optMatch) {
|
|
1146
|
+
const someExpr = transformPureBody(optMatch.someBranch, typeDecls);
|
|
1048
1147
|
if (!someExpr)
|
|
1049
1148
|
return null;
|
|
1050
|
-
const noneExpr = transformPureBody(noneBranch, typeDecls);
|
|
1149
|
+
const noneExpr = transformPureBody(optMatch.noneBranch, typeDecls);
|
|
1051
1150
|
if (!noneExpr)
|
|
1052
1151
|
return null;
|
|
1053
|
-
const someReplaced = replaceVar(someExpr,
|
|
1152
|
+
const someReplaced = replaceVar(someExpr, optMatch.check.varName, { kind: "var", name: optMatch.bound });
|
|
1054
1153
|
return {
|
|
1055
|
-
kind: "match", scrutinee:
|
|
1154
|
+
kind: "match", scrutinee: optMatch.check.varName,
|
|
1056
1155
|
arms: [
|
|
1057
|
-
{ pattern: `.some ${bound}`, body: someReplaced },
|
|
1156
|
+
{ pattern: `.some ${optMatch.bound}`, body: someReplaced },
|
|
1058
1157
|
{ pattern: ".none", body: noneExpr },
|
|
1059
1158
|
],
|
|
1060
1159
|
};
|
|
@@ -1075,22 +1174,21 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1075
1174
|
return null;
|
|
1076
1175
|
}
|
|
1077
1176
|
function transformPureSwitch(s, typeDecls) {
|
|
1078
|
-
const
|
|
1079
|
-
if (!
|
|
1177
|
+
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
|
|
1178
|
+
if (!typeDecls.find(d => d.name === typeName))
|
|
1080
1179
|
return null;
|
|
1081
1180
|
const varName = s.expr.kind === "var" ? s.expr.name : undefined;
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name, varName)).join(" ")}` : `.${c.label}`;
|
|
1087
|
-
let body = transformPureBody(c.body, typeDecls);
|
|
1088
|
-
if (!body)
|
|
1181
|
+
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1182
|
+
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => {
|
|
1183
|
+
let result = transformPureBody(body, typeDecls);
|
|
1184
|
+
if (!result)
|
|
1089
1185
|
return null;
|
|
1090
|
-
if (fields.length > 0 &&
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1186
|
+
if (fields.length > 0 && vn)
|
|
1187
|
+
result = replaceFieldAccess(result, vn, fields);
|
|
1188
|
+
return result;
|
|
1189
|
+
});
|
|
1190
|
+
if (!arms)
|
|
1191
|
+
return null;
|
|
1094
1192
|
if (s.defaultBody.length > 0) {
|
|
1095
1193
|
const body = transformPureBody(s.defaultBody, typeDecls);
|
|
1096
1194
|
if (!body)
|
|
@@ -1102,22 +1200,21 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
1102
1200
|
return { kind: "match", scrutinee: s.expr.name, arms };
|
|
1103
1201
|
}
|
|
1104
1202
|
function transformPureMatch(chain, typeDecls) {
|
|
1105
|
-
const
|
|
1106
|
-
const arms =
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
const fields = variant?.fields ?? [];
|
|
1110
|
-
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name, chain.varName)).join(" ")}` : `.${c.variant}`;
|
|
1111
|
-
let body = transformPureBody(c.body, typeDecls);
|
|
1112
|
-
if (!body)
|
|
1203
|
+
const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
|
|
1204
|
+
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => {
|
|
1205
|
+
let result = transformPureBody(body, typeDecls);
|
|
1206
|
+
if (!result)
|
|
1113
1207
|
return null;
|
|
1114
|
-
if (fields.length > 0)
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1208
|
+
if (fields.length > 0 && vn)
|
|
1209
|
+
result = replaceFieldAccess(result, vn, fields);
|
|
1210
|
+
return result;
|
|
1211
|
+
});
|
|
1212
|
+
if (!arms)
|
|
1213
|
+
return null;
|
|
1118
1214
|
// Idiomatic TS often has an unreachable fallthrough after exhaustive if-chains on
|
|
1119
1215
|
// discriminated unions. Skip the catch-all arm when all variants are matched,
|
|
1120
1216
|
// since Lean errors on redundant match arms.
|
|
1217
|
+
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
1121
1218
|
const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
|
|
1122
1219
|
if (chain.fallthrough.length > 0 && !allCovered) {
|
|
1123
1220
|
const body = transformPureBody(chain.fallthrough, typeDecls);
|
|
@@ -1142,7 +1239,7 @@ function transformTypeDecl(d) {
|
|
|
1142
1239
|
typeParams: d.typeParams,
|
|
1143
1240
|
constructors: d.variants.map(v => ({
|
|
1144
1241
|
name: v.name,
|
|
1145
|
-
fields: v.fields.map(f => ({ name: f.name, type:
|
|
1242
|
+
fields: v.fields.map(f => ({ name: f.name, type: f.type })),
|
|
1146
1243
|
})),
|
|
1147
1244
|
deriving: ["Repr", "Inhabited"],
|
|
1148
1245
|
};
|
|
@@ -1150,13 +1247,13 @@ function transformTypeDecl(d) {
|
|
|
1150
1247
|
else if (d.kind === "alias") {
|
|
1151
1248
|
return {
|
|
1152
1249
|
kind: "type-alias", name: d.name,
|
|
1153
|
-
target:
|
|
1250
|
+
target: d.aliasOfTy,
|
|
1154
1251
|
};
|
|
1155
1252
|
}
|
|
1156
1253
|
else {
|
|
1157
1254
|
return {
|
|
1158
1255
|
kind: "structure", name: d.name,
|
|
1159
|
-
fields: d.fields.map(f => ({ name: f.name, type:
|
|
1256
|
+
fields: d.fields.map(f => ({ name: f.name, type: f.type })),
|
|
1160
1257
|
deriving: ["Repr", "Inhabited", "DecidableEq"],
|
|
1161
1258
|
};
|
|
1162
1259
|
}
|
|
@@ -1235,6 +1332,7 @@ export function transformModuleDafny(mod) {
|
|
|
1235
1332
|
}
|
|
1236
1333
|
export function transformModule(mod, specImport) {
|
|
1237
1334
|
_forofCounters.clear();
|
|
1335
|
+
_liftCounter = 0;
|
|
1238
1336
|
_typeDecls = mod.typeDecls;
|
|
1239
1337
|
const typeDecls = mod.typeDecls.map(transformTypeDecl);
|
|
1240
1338
|
// Module-level constants
|