lemmascript 0.3.0 → 0.3.1
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 +87 -155
- package/tools/dist/extract.js +3 -1
- package/tools/dist/lsc.js +7 -1
- package/tools/dist/resolve.js +218 -178
- package/tools/dist/transform.js +217 -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
|
|
@@ -427,23 +437,41 @@ function lowerExpr(e, binds) {
|
|
|
427
437
|
}
|
|
428
438
|
}
|
|
429
439
|
}
|
|
430
|
-
// For spread records,
|
|
440
|
+
// For spread records, propagate declared field types and wrap optionals
|
|
431
441
|
if (e.spread) {
|
|
432
442
|
const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
|
|
433
443
|
const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
|
|
434
444
|
const structDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "record") : undefined;
|
|
445
|
+
// Also check discriminated-union variants for field types
|
|
446
|
+
const unionDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "discriminated-union") : undefined;
|
|
435
447
|
const loweredFields = e.fields.map(f => {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
448
|
+
// Propagate declared field type onto value if it has unknown type
|
|
449
|
+
let fieldValue = f.value;
|
|
450
|
+
const fieldDecl = structDecl?.fields?.find(sf => sf.name === f.name);
|
|
451
|
+
let declaredTy;
|
|
452
|
+
if (fieldDecl) {
|
|
453
|
+
declaredTy = fieldDecl.type;
|
|
454
|
+
}
|
|
455
|
+
else if (unionDecl?.variants) {
|
|
456
|
+
for (const v of unionDecl.variants) {
|
|
457
|
+
const vf = v.fields.find(vf => vf.name === f.name);
|
|
458
|
+
if (vf) {
|
|
459
|
+
declaredTy = vf.type;
|
|
460
|
+
break;
|
|
444
461
|
}
|
|
445
462
|
}
|
|
446
463
|
}
|
|
464
|
+
if (declaredTy && fieldValue.ty.kind === "unknown") {
|
|
465
|
+
fieldValue = { ...fieldValue, ty: declaredTy };
|
|
466
|
+
}
|
|
467
|
+
let value = lowerExpr(fieldValue, binds);
|
|
468
|
+
// Wrap non-optional values in Some for optional fields
|
|
469
|
+
if (declaredTy?.kind === "optional") {
|
|
470
|
+
const isUndef = f.value.kind === "var" && f.value.name === "undefined";
|
|
471
|
+
if (f.value.ty.kind !== "optional" && !isUndef) {
|
|
472
|
+
value = { kind: "app", fn: "Some", args: [value] };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
447
475
|
return { name: f.name, value };
|
|
448
476
|
});
|
|
449
477
|
return { kind: "record", spread: lowerExpr(e.spread, binds), fields: loweredFields };
|
|
@@ -466,7 +494,9 @@ function lowerExpr(e, binds) {
|
|
|
466
494
|
case "exists":
|
|
467
495
|
return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
468
496
|
case "conditional": {
|
|
469
|
-
|
|
497
|
+
// When narrowedExpr is set, the match replaces the condition — don't lift from it
|
|
498
|
+
const condBinds = (e.narrowedVar && e.narrowedExpr) ? null : binds;
|
|
499
|
+
const cond = lowerExpr(e.cond, condBinds);
|
|
470
500
|
let thenExpr = lowerExpr(e.then, binds);
|
|
471
501
|
let elseExpr = lowerExpr(e.else, binds);
|
|
472
502
|
// Explicit !== undefined with narrowedExpr → match Some/None on the optional expression
|
|
@@ -476,11 +506,11 @@ function lowerExpr(e, binds) {
|
|
|
476
506
|
if (bound !== e.narrowedVar) {
|
|
477
507
|
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
478
508
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
509
|
+
// Wrap in Some/None only when result is optional (one branch is undefined)
|
|
510
|
+
if (e.ty.kind === "optional") {
|
|
511
|
+
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
512
|
+
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
513
|
+
}
|
|
484
514
|
return {
|
|
485
515
|
kind: "match", scrutinee,
|
|
486
516
|
arms: [
|
|
@@ -497,12 +527,8 @@ function lowerExpr(e, binds) {
|
|
|
497
527
|
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
498
528
|
}
|
|
499
529
|
// 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);
|
|
530
|
+
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
531
|
+
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
506
532
|
return {
|
|
507
533
|
kind: "match", scrutinee: cond,
|
|
508
534
|
arms: [
|
|
@@ -513,18 +539,8 @@ function lowerExpr(e, binds) {
|
|
|
513
539
|
}
|
|
514
540
|
// Non-optional: regular if with optional wrapping
|
|
515
541
|
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
|
-
}
|
|
542
|
+
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
543
|
+
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
528
544
|
}
|
|
529
545
|
return { kind: "if", cond, then: thenExpr, else: elseExpr };
|
|
530
546
|
}
|
|
@@ -571,7 +587,7 @@ function ensuresToMatch(e, typeDecls) {
|
|
|
571
587
|
if (!variant)
|
|
572
588
|
return null;
|
|
573
589
|
const fields = variant.fields;
|
|
574
|
-
const pattern =
|
|
590
|
+
const pattern = buildMatchPattern(variantName, fields, obj.name);
|
|
575
591
|
let rhs = transformExpr(e.right);
|
|
576
592
|
rhs = replaceFieldAccess(rhs, obj.name, fields);
|
|
577
593
|
return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
|
|
@@ -604,13 +620,12 @@ function transformStmts(stmts, typeDecls) {
|
|
|
604
620
|
continue;
|
|
605
621
|
}
|
|
606
622
|
// Detect optional check → match on Some/None
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
609
|
-
|
|
610
|
-
result.push(emitOptionalMatch(opt.varName, opt.negated, s, typeDecls, rest));
|
|
623
|
+
const optMatch = prepareOptionalMatch(s, stmts.slice(i + 1));
|
|
624
|
+
if (optMatch) {
|
|
625
|
+
result.push(emitOptionalMatch(optMatch.check.varName, optMatch.check.negated, s, typeDecls, stmts.slice(i + 1), optMatch.check.fieldExpr));
|
|
611
626
|
// If rest was consumed into the Some branch, skip remaining
|
|
612
|
-
const
|
|
613
|
-
if (
|
|
627
|
+
const origSome = optMatch.check.negated ? s.else : s.then;
|
|
628
|
+
if (origSome.length === 0 && i + 1 < stmts.length) {
|
|
614
629
|
return result;
|
|
615
630
|
}
|
|
616
631
|
i++;
|
|
@@ -894,7 +909,7 @@ function parseDiscriminantCond(cond) {
|
|
|
894
909
|
return null;
|
|
895
910
|
return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
|
|
896
911
|
}
|
|
897
|
-
function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
|
|
912
|
+
function emitOptionalMatch(varName, negated, s, typeDecls, restStmts, fieldExpr) {
|
|
898
913
|
let someBranch = negated ? s.else : s.then;
|
|
899
914
|
const noneBranch = negated ? s.then : s.else;
|
|
900
915
|
// Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
|
|
@@ -903,19 +918,69 @@ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
|
|
|
903
918
|
someBranch = restStmts;
|
|
904
919
|
}
|
|
905
920
|
const bound = matchBinder(`${varName}_val`);
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
921
|
+
// Replace the narrowed variable/field in the Some branch body.
|
|
922
|
+
// Field chains: replace in TStmt before transform (so downstream narrowing sees simple vars).
|
|
923
|
+
// Simple vars: replace in IR after transform (the original mechanism).
|
|
924
|
+
let someBody;
|
|
925
|
+
if (fieldExpr && fieldExpr.kind === "field" && fieldExpr.obj.kind === "var") {
|
|
926
|
+
const innerTy = fieldExpr.ty.kind === "optional" ? fieldExpr.ty.inner : fieldExpr.ty;
|
|
927
|
+
const replaced = replaceFieldsInTStmts(someBranch, fieldExpr.obj.name, [
|
|
928
|
+
{ fieldName: fieldExpr.field, newName: bound, fallbackTy: innerTy },
|
|
929
|
+
]);
|
|
930
|
+
someBody = transformStmts(replaced, typeDecls);
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
const transformed = transformStmts(someBranch, typeDecls);
|
|
934
|
+
someBody = transformed.map(stmt => mapStmtExprs(stmt, e => replaceVar(e, varName, { kind: "var", name: bound })));
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
kind: "match", scrutinee: varName,
|
|
938
|
+
arms: [
|
|
939
|
+
{ pattern: `.some ${bound}`, body: someBody },
|
|
940
|
+
{ pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
|
|
941
|
+
],
|
|
942
|
+
};
|
|
914
943
|
}
|
|
915
944
|
/** Apply an expression transform to all expressions in a statement (convenience wrapper). */
|
|
916
945
|
function mapStmtExprs(s, r) {
|
|
917
946
|
return mapStmt(s, e => r(e));
|
|
918
947
|
}
|
|
948
|
+
// ── Optional narrowing helpers ──────────────────────────────
|
|
949
|
+
//
|
|
950
|
+
// Optional narrowing converts TS `if (x === undefined)` patterns to Dafny
|
|
951
|
+
// `match x { Some(val) => ..., None => ... }`.
|
|
952
|
+
//
|
|
953
|
+
// The resolve phase (resolve.ts) handles:
|
|
954
|
+
// - Flow narrowing: after `if (x === undefined) return`, x is non-optional
|
|
955
|
+
// - && narrowing: in `x !== undefined && f(x)`, f(x) sees x as non-optional
|
|
956
|
+
// - Conditional narrowing: in `x !== undefined ? x.field : default`, sets
|
|
957
|
+
// narrowedVar/narrowedExpr on TExpr for the transform phase
|
|
958
|
+
//
|
|
959
|
+
// The transform phase (here) handles:
|
|
960
|
+
// - Statement-level: `transformStmts` detects optional checks → `emitOptionalMatch`
|
|
961
|
+
// - Expression-level: `lowerExpr` conditional reads narrowedVar/narrowedExpr → match
|
|
962
|
+
// - && restructuring: `extractLeftmostOptional` splits `&&` chains into nested ifs
|
|
963
|
+
// so `emitOptionalMatch` can detect the inner optional check
|
|
964
|
+
//
|
|
965
|
+
// Both phases detect `v !== undefined` patterns. The resolve phase uses
|
|
966
|
+
// `detectOptionalCheck` (on RawExpr), the transform uses `parseOptionalCheck` (on TExpr).
|
|
967
|
+
// These are separate because they operate on different IR types, but both handle
|
|
968
|
+
// simple variables and field access chains.
|
|
969
|
+
/** Shared logic for optional match in both imperative and pure function paths.
|
|
970
|
+
* Detects optional check, selects branches, handles early-return consumption.
|
|
971
|
+
* Returns null if the condition is not an optional check. */
|
|
972
|
+
function prepareOptionalMatch(s, restStmts) {
|
|
973
|
+
const check = parseOptionalCheck(s.cond);
|
|
974
|
+
if (!check)
|
|
975
|
+
return null;
|
|
976
|
+
let someBranch = check.negated ? s.else : s.then;
|
|
977
|
+
const noneBranch = check.negated ? s.then : (s.else.length > 0 ? s.else : restStmts);
|
|
978
|
+
// Early-return pattern: Some branch is empty → consume rest of block
|
|
979
|
+
if (someBranch.length === 0 && restStmts.length > 0)
|
|
980
|
+
someBranch = restStmts;
|
|
981
|
+
const bound = matchBinder(`${check.varName}_val`);
|
|
982
|
+
return { check, someBranch, noneBranch, bound };
|
|
983
|
+
}
|
|
919
984
|
/** Extract the leftmost optional check from a && chain, returning the check and the rest.
|
|
920
985
|
* (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
|
|
921
986
|
function extractLeftmostOptional(cond) {
|
|
@@ -931,7 +996,9 @@ function extractLeftmostOptional(cond) {
|
|
|
931
996
|
}
|
|
932
997
|
return null;
|
|
933
998
|
}
|
|
934
|
-
/** Detect `v !== undefined` or `undefined !== v` where v has optional type.
|
|
999
|
+
/** Detect `v !== undefined` or `undefined !== v` where v has optional type.
|
|
1000
|
+
* Also handles field access chains like `obj.field !== undefined`.
|
|
1001
|
+
* When `fieldExpr` is returned, callers must use field-aware replacement. */
|
|
935
1002
|
function parseOptionalCheck(cond) {
|
|
936
1003
|
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
|
|
937
1004
|
return null;
|
|
@@ -940,21 +1007,50 @@ function parseOptionalCheck(cond) {
|
|
|
940
1007
|
varExpr = cond.left;
|
|
941
1008
|
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
942
1009
|
varExpr = cond.right;
|
|
943
|
-
if (!varExpr
|
|
1010
|
+
if (!varExpr)
|
|
944
1011
|
return null;
|
|
945
|
-
|
|
1012
|
+
if (varExpr.kind === "var" && varExpr.ty.kind === "optional") {
|
|
1013
|
+
return { varName: varExpr.name, negated: cond.op === "===" };
|
|
1014
|
+
}
|
|
1015
|
+
if (varExpr.kind === "field" && varExpr.ty.kind === "optional") {
|
|
1016
|
+
// Serialize field chain as a dotted name for use as match scrutinee
|
|
1017
|
+
const chain = serializeFieldChain(varExpr);
|
|
1018
|
+
if (chain)
|
|
1019
|
+
return { varName: chain, negated: cond.op === "===", fieldExpr: varExpr };
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
946
1022
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1023
|
+
/** Serialize a field access chain to a dotted variable path, or null if not a simple chain. */
|
|
1024
|
+
function serializeFieldChain(e) {
|
|
1025
|
+
if (e.kind === "var")
|
|
1026
|
+
return e.name;
|
|
1027
|
+
if (e.kind === "field") {
|
|
1028
|
+
const parent = serializeFieldChain(e.obj);
|
|
1029
|
+
return parent ? `${parent}.${e.field}` : null;
|
|
1030
|
+
}
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
/** Build match arms from variant cases — shared by imperative and pure paths.
|
|
1034
|
+
* Looks up variant fields from typeDecls, builds patterns via buildMatchPattern,
|
|
1035
|
+
* and delegates body transformation to the caller-provided function.
|
|
1036
|
+
* Returns null if any body transformation returns null (pure path abort). */
|
|
1037
|
+
function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
1038
|
+
const decl = typeName ? typeDecls.find(d => d.name === typeName) : undefined;
|
|
1039
|
+
const arms = [];
|
|
1040
|
+
for (const c of cases) {
|
|
1041
|
+
const variant = decl?.variants?.find(v => v.name === c.name);
|
|
951
1042
|
const fields = variant?.fields ?? [];
|
|
952
|
-
const pattern =
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
1043
|
+
const pattern = buildMatchPattern(c.name, fields, varName);
|
|
1044
|
+
const body = transformBody(c.body, varName, fields);
|
|
1045
|
+
if (body === null)
|
|
1046
|
+
return null;
|
|
1047
|
+
arms.push({ pattern, body });
|
|
1048
|
+
}
|
|
1049
|
+
return arms;
|
|
1050
|
+
}
|
|
1051
|
+
function emitMatchStmt(chain, typeDecls) {
|
|
1052
|
+
const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
|
|
1053
|
+
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
|
|
958
1054
|
if (chain.fallthrough.length > 0)
|
|
959
1055
|
arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
|
|
960
1056
|
return { kind: "match", scrutinee: chain.varName, arms };
|
|
@@ -962,59 +1058,39 @@ function emitMatchStmt(chain, typeDecls) {
|
|
|
962
1058
|
function emitSwitchStmt(s, typeDecls) {
|
|
963
1059
|
const varName = s.expr.kind === "var" ? s.expr.name : "?";
|
|
964
1060
|
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
|
-
});
|
|
1061
|
+
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1062
|
+
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
|
|
975
1063
|
if (s.defaultBody.length > 0)
|
|
976
1064
|
arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
|
|
977
1065
|
return { kind: "match", scrutinee: varName, arms };
|
|
978
1066
|
}
|
|
979
|
-
/** Replace obj.field →
|
|
980
|
-
*
|
|
981
|
-
*
|
|
982
|
-
|
|
983
|
-
|
|
1067
|
+
/** Replace obj.field → replacement var in typed IR (before transform).
|
|
1068
|
+
* Used by discriminant match/switch and optional match to rewrite field accesses
|
|
1069
|
+
* into simple variables before the transform phase, so downstream narrowing
|
|
1070
|
+
* (parseOptionalCheck, extractLeftmostOptional) sees simple variable references.
|
|
1071
|
+
* Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
|
|
1072
|
+
function replaceFieldsInTStmts(stmts, objName, replacements) {
|
|
1073
|
+
if (replacements.length === 0)
|
|
984
1074
|
return stmts;
|
|
985
1075
|
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:
|
|
1076
|
+
if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === objName) {
|
|
1077
|
+
const r = replacements.find(r => r.fieldName === e.field);
|
|
1078
|
+
if (r) {
|
|
1079
|
+
const ty = e.ty.kind !== "unknown" ? e.ty : r.fallbackTy;
|
|
1080
|
+
return { kind: "var", name: r.newName, ty };
|
|
991
1081
|
}
|
|
992
1082
|
}
|
|
993
1083
|
return null;
|
|
994
1084
|
}));
|
|
995
1085
|
}
|
|
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;
|
|
1086
|
+
/** Replace all variant fields of obj → match binder vars in typed IR.
|
|
1087
|
+
* Thin wrapper around replaceFieldsInTStmts for discriminant match/switch. */
|
|
1088
|
+
function replaceFieldAccessInTStmts(stmts, varName, fields) {
|
|
1089
|
+
return replaceFieldsInTStmts(stmts, varName, fields.map(f => ({
|
|
1090
|
+
fieldName: f.name,
|
|
1091
|
+
newName: matchBinder(f.name, varName),
|
|
1092
|
+
fallbackTy: f.type ?? parseTsType(f.tsType),
|
|
1093
|
+
})));
|
|
1018
1094
|
}
|
|
1019
1095
|
// ── Pure function generation ─────────────────────────────────
|
|
1020
1096
|
function transformPureBody(stmts, typeDecls) {
|
|
@@ -1037,24 +1113,19 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1037
1113
|
}
|
|
1038
1114
|
case "if": {
|
|
1039
1115
|
// 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);
|
|
1116
|
+
const optMatch = prepareOptionalMatch(s, rest);
|
|
1117
|
+
if (optMatch) {
|
|
1118
|
+
const someExpr = transformPureBody(optMatch.someBranch, typeDecls);
|
|
1048
1119
|
if (!someExpr)
|
|
1049
1120
|
return null;
|
|
1050
|
-
const noneExpr = transformPureBody(noneBranch, typeDecls);
|
|
1121
|
+
const noneExpr = transformPureBody(optMatch.noneBranch, typeDecls);
|
|
1051
1122
|
if (!noneExpr)
|
|
1052
1123
|
return null;
|
|
1053
|
-
const someReplaced = replaceVar(someExpr,
|
|
1124
|
+
const someReplaced = replaceVar(someExpr, optMatch.check.varName, { kind: "var", name: optMatch.bound });
|
|
1054
1125
|
return {
|
|
1055
|
-
kind: "match", scrutinee:
|
|
1126
|
+
kind: "match", scrutinee: optMatch.check.varName,
|
|
1056
1127
|
arms: [
|
|
1057
|
-
{ pattern: `.some ${bound}`, body: someReplaced },
|
|
1128
|
+
{ pattern: `.some ${optMatch.bound}`, body: someReplaced },
|
|
1058
1129
|
{ pattern: ".none", body: noneExpr },
|
|
1059
1130
|
],
|
|
1060
1131
|
};
|
|
@@ -1075,22 +1146,21 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1075
1146
|
return null;
|
|
1076
1147
|
}
|
|
1077
1148
|
function transformPureSwitch(s, typeDecls) {
|
|
1078
|
-
const
|
|
1079
|
-
if (!
|
|
1149
|
+
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
|
|
1150
|
+
if (!typeDecls.find(d => d.name === typeName))
|
|
1080
1151
|
return null;
|
|
1081
1152
|
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)
|
|
1153
|
+
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1154
|
+
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => {
|
|
1155
|
+
let result = transformPureBody(body, typeDecls);
|
|
1156
|
+
if (!result)
|
|
1089
1157
|
return null;
|
|
1090
|
-
if (fields.length > 0 &&
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1158
|
+
if (fields.length > 0 && vn)
|
|
1159
|
+
result = replaceFieldAccess(result, vn, fields);
|
|
1160
|
+
return result;
|
|
1161
|
+
});
|
|
1162
|
+
if (!arms)
|
|
1163
|
+
return null;
|
|
1094
1164
|
if (s.defaultBody.length > 0) {
|
|
1095
1165
|
const body = transformPureBody(s.defaultBody, typeDecls);
|
|
1096
1166
|
if (!body)
|
|
@@ -1102,22 +1172,21 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
1102
1172
|
return { kind: "match", scrutinee: s.expr.name, arms };
|
|
1103
1173
|
}
|
|
1104
1174
|
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)
|
|
1175
|
+
const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
|
|
1176
|
+
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => {
|
|
1177
|
+
let result = transformPureBody(body, typeDecls);
|
|
1178
|
+
if (!result)
|
|
1113
1179
|
return null;
|
|
1114
|
-
if (fields.length > 0)
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1180
|
+
if (fields.length > 0 && vn)
|
|
1181
|
+
result = replaceFieldAccess(result, vn, fields);
|
|
1182
|
+
return result;
|
|
1183
|
+
});
|
|
1184
|
+
if (!arms)
|
|
1185
|
+
return null;
|
|
1118
1186
|
// Idiomatic TS often has an unreachable fallthrough after exhaustive if-chains on
|
|
1119
1187
|
// discriminated unions. Skip the catch-all arm when all variants are matched,
|
|
1120
1188
|
// since Lean errors on redundant match arms.
|
|
1189
|
+
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
1121
1190
|
const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
|
|
1122
1191
|
if (chain.fallthrough.length > 0 && !allCovered) {
|
|
1123
1192
|
const body = transformPureBody(chain.fallthrough, typeDecls);
|
|
@@ -1142,7 +1211,7 @@ function transformTypeDecl(d) {
|
|
|
1142
1211
|
typeParams: d.typeParams,
|
|
1143
1212
|
constructors: d.variants.map(v => ({
|
|
1144
1213
|
name: v.name,
|
|
1145
|
-
fields: v.fields.map(f => ({ name: f.name, type:
|
|
1214
|
+
fields: v.fields.map(f => ({ name: f.name, type: f.type })),
|
|
1146
1215
|
})),
|
|
1147
1216
|
deriving: ["Repr", "Inhabited"],
|
|
1148
1217
|
};
|
|
@@ -1150,13 +1219,13 @@ function transformTypeDecl(d) {
|
|
|
1150
1219
|
else if (d.kind === "alias") {
|
|
1151
1220
|
return {
|
|
1152
1221
|
kind: "type-alias", name: d.name,
|
|
1153
|
-
target:
|
|
1222
|
+
target: d.aliasOfTy,
|
|
1154
1223
|
};
|
|
1155
1224
|
}
|
|
1156
1225
|
else {
|
|
1157
1226
|
return {
|
|
1158
1227
|
kind: "structure", name: d.name,
|
|
1159
|
-
fields: d.fields.map(f => ({ name: f.name, type:
|
|
1228
|
+
fields: d.fields.map(f => ({ name: f.name, type: f.type })),
|
|
1160
1229
|
deriving: ["Repr", "Inhabited", "DecidableEq"],
|
|
1161
1230
|
};
|
|
1162
1231
|
}
|
|
@@ -1235,6 +1304,7 @@ export function transformModuleDafny(mod) {
|
|
|
1235
1304
|
}
|
|
1236
1305
|
export function transformModule(mod, specImport) {
|
|
1237
1306
|
_forofCounters.clear();
|
|
1307
|
+
_liftCounter = 0;
|
|
1238
1308
|
_typeDecls = mod.typeDecls;
|
|
1239
1309
|
const typeDecls = mod.typeDecls.map(transformTypeDecl);
|
|
1240
1310
|
// Module-level constants
|