lemmascript 0.3.1 → 0.3.3
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 +1 -0
- package/package.json +3 -2
- package/tools/dist/dafny-emit.js +70 -12
- package/tools/dist/extract.js +182 -3
- package/tools/dist/lean-emit.js +2 -0
- package/tools/dist/resolve.js +128 -37
- package/tools/dist/transform.js +307 -58
package/tools/dist/transform.js
CHANGED
|
@@ -58,7 +58,10 @@ function mapStmt(s, f) {
|
|
|
58
58
|
case "break":
|
|
59
59
|
case "continue": return s;
|
|
60
60
|
case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapStmt(t, f)), else: s.else.map(t => mapStmt(t, f)) };
|
|
61
|
-
case "match":
|
|
61
|
+
case "match": {
|
|
62
|
+
const scr = typeof s.scrutinee === "string" ? s.scrutinee : r(s.scrutinee);
|
|
63
|
+
return { ...s, scrutinee: scr, arms: s.arms.map(a => ({ ...a, body: a.body.map(t => mapStmt(t, f)) })) };
|
|
64
|
+
}
|
|
62
65
|
case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
|
|
63
66
|
case "forin": return { ...s, bound: r(s.bound), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
|
|
64
67
|
case "ghostLet": return { ...s, value: r(s.value) };
|
|
@@ -347,9 +350,24 @@ function lowerExpr(e, binds) {
|
|
|
347
350
|
return { kind: "field", obj: transformExpr(e.obj), field: "length" };
|
|
348
351
|
if (e.field === "size" && (e.obj.ty.kind === "map" || e.obj.ty.kind === "set"))
|
|
349
352
|
return { kind: "field", obj: transformExpr(e.obj), field: "collectionSize" };
|
|
353
|
+
// Boolean discriminant bare access: `result.ok` where Result has variants
|
|
354
|
+
// {ok: true, ...} | {ok: false, ...}. String discriminants are always used via
|
|
355
|
+
// comparison (x.kind === 'Foo' → x.Foo?), but boolean discriminants are used
|
|
356
|
+
// as bare truthiness checks. Emit as the Dafny discriminator predicate for the
|
|
357
|
+
// 'true' variant: result.ok → result.true_?
|
|
358
|
+
if (e.isDiscriminant && e.obj.ty.kind === "user") {
|
|
359
|
+
const baseName = e.obj.ty.name.includes("<") ? e.obj.ty.name.slice(0, e.obj.ty.name.indexOf("<")) : e.obj.ty.name;
|
|
360
|
+
const decl = _typeDecls.find(d => d.name === baseName && d.kind === "discriminated-union");
|
|
361
|
+
if (decl?.variants?.some(v => v.name === "true")) {
|
|
362
|
+
return { kind: "field", obj: transformExpr(e.obj), field: "true_?" };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
350
365
|
return { kind: "field", obj: transformExpr(e.obj), field: e.field };
|
|
351
366
|
case "index": {
|
|
352
367
|
const idx = transformExpr(e.idx);
|
|
368
|
+
if (e.obj.ty.kind === "map") {
|
|
369
|
+
return { kind: "methodCall", obj: transformExpr(e.obj), objTy: e.obj.ty, method: "get", args: [idx], monadic: false };
|
|
370
|
+
}
|
|
353
371
|
const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
|
|
354
372
|
return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
|
|
355
373
|
}
|
|
@@ -394,6 +412,15 @@ function lowerExpr(e, binds) {
|
|
|
394
412
|
if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
|
|
395
413
|
method = "getDirect";
|
|
396
414
|
}
|
|
415
|
+
// map.set(k, v): if v is an Optional-wrapped map get, unwrap to getDirect
|
|
416
|
+
// (the desugared spread { ...m, [k]: m2[k] } becomes m.set(k, m2.get(k)),
|
|
417
|
+
// but the value should be direct access, not Optional)
|
|
418
|
+
if (method === "set" && e.fn.obj.ty.kind === "map" && args.length === 2) {
|
|
419
|
+
const val = args[1];
|
|
420
|
+
if (val.kind === "methodCall" && val.method === "get" && val.objTy.kind === "map") {
|
|
421
|
+
args[1] = { ...val, method: "getDirect" };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
397
424
|
// Check if any lambda arg has monadic body
|
|
398
425
|
const needsMonadic = _opts.monadic && args.some(a => a.kind === "lambda" && isMonadicBody(a.body));
|
|
399
426
|
const result = { kind: "methodCall", obj: recv, objTy: e.fn.obj.ty, method, args, monadic: needsMonadic };
|
|
@@ -476,6 +503,10 @@ function lowerExpr(e, binds) {
|
|
|
476
503
|
});
|
|
477
504
|
return { kind: "record", spread: lowerExpr(e.spread, binds), fields: loweredFields };
|
|
478
505
|
}
|
|
506
|
+
// Empty record with map type → empty map
|
|
507
|
+
if (e.fields.length === 0 && !e.spread && e.ty.kind === "map") {
|
|
508
|
+
return { kind: "emptyMap" };
|
|
509
|
+
}
|
|
479
510
|
return { kind: "record", spread: null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
|
|
480
511
|
}
|
|
481
512
|
case "arrayLiteral":
|
|
@@ -494,19 +525,16 @@ function lowerExpr(e, binds) {
|
|
|
494
525
|
case "exists":
|
|
495
526
|
return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
496
527
|
case "conditional": {
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
const cond = lowerExpr(e.cond, condBinds);
|
|
500
|
-
let thenExpr = lowerExpr(e.then, binds);
|
|
501
|
-
let elseExpr = lowerExpr(e.else, binds);
|
|
502
|
-
// Explicit !== undefined with narrowedExpr → match Some/None on the optional expression
|
|
528
|
+
// Phase 0: Complex expression check (call results, etc.) — resolve substituted
|
|
529
|
+
// and set narrowedVar + narrowedExpr because transform can't detect these.
|
|
503
530
|
if (e.narrowedVar && e.narrowedExpr) {
|
|
504
531
|
const scrutinee = lowerExpr(e.narrowedExpr, binds);
|
|
505
532
|
const bound = matchBinder(e.narrowedVar);
|
|
533
|
+
let thenExpr = lowerExpr(e.then, binds);
|
|
534
|
+
let elseExpr = lowerExpr(e.else, binds);
|
|
506
535
|
if (bound !== e.narrowedVar) {
|
|
507
536
|
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
508
537
|
}
|
|
509
|
-
// Wrap in Some/None only when result is optional (one branch is undefined)
|
|
510
538
|
if (e.ty.kind === "optional") {
|
|
511
539
|
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
512
540
|
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
@@ -519,14 +547,16 @@ function lowerExpr(e, binds) {
|
|
|
519
547
|
],
|
|
520
548
|
};
|
|
521
549
|
}
|
|
522
|
-
//
|
|
550
|
+
// Phase 1: Truthiness — cond itself is optional (e.g. opt ? X : Y)
|
|
551
|
+
// Uses narrowedVar set by resolve's Phase 1 (unchanged).
|
|
523
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);
|
|
524
556
|
const bound = matchBinder(e.narrowedVar);
|
|
525
|
-
// Replace the synthetic/narrowed var with the match-bound name
|
|
526
557
|
if (bound !== e.narrowedVar) {
|
|
527
558
|
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
528
559
|
}
|
|
529
|
-
// The match produces an Optional: wrap branches in Some/None.
|
|
530
560
|
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
531
561
|
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
532
562
|
return {
|
|
@@ -537,7 +567,26 @@ function lowerExpr(e, binds) {
|
|
|
537
567
|
],
|
|
538
568
|
};
|
|
539
569
|
}
|
|
540
|
-
//
|
|
570
|
+
// Phase 2: Explicit check — x !== undefined ? A : B
|
|
571
|
+
// Transform detects the pattern itself (no narrowedVar/narrowedExpr from resolve).
|
|
572
|
+
const check = parseOptionalCheck(e.cond);
|
|
573
|
+
if (check && !check.negated) {
|
|
574
|
+
return lowerOptionalConditional(e, check, null, binds);
|
|
575
|
+
}
|
|
576
|
+
// Phase 3: && with optional check — x !== undefined && guard(x) ? A : B
|
|
577
|
+
if (e.cond.kind === "binop" && e.cond.op === "&&") {
|
|
578
|
+
const extracted = extractLeftmostOptional(e.cond);
|
|
579
|
+
if (extracted) {
|
|
580
|
+
const innerCheck = parseOptionalCheck(extracted.optCond);
|
|
581
|
+
if (innerCheck && !innerCheck.negated) {
|
|
582
|
+
return lowerOptionalConditional(e, innerCheck, extracted.rest, binds);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Phase 4: Regular conditional (no optional narrowing)
|
|
587
|
+
const cond = lowerExpr(e.cond, binds);
|
|
588
|
+
let thenExpr = lowerExpr(e.then, binds);
|
|
589
|
+
let elseExpr = lowerExpr(e.else, binds);
|
|
541
590
|
if (e.ty.kind === "optional") {
|
|
542
591
|
thenExpr = wrapOptionalBranch(thenExpr, e.then);
|
|
543
592
|
elseExpr = wrapOptionalBranch(elseExpr, e.else);
|
|
@@ -637,18 +686,43 @@ function transformStmts(stmts, typeDecls) {
|
|
|
637
686
|
const varName = s.names[0];
|
|
638
687
|
const varTy = s.nameTypes[0] ?? { kind: "unknown" };
|
|
639
688
|
let iterExpr = transformExpr(s.iterable);
|
|
689
|
+
// Map key-only iteration: for (const k in record) → iterate keys only
|
|
690
|
+
if (s.names.length === 1 && s.iterable.ty.kind === "map") {
|
|
691
|
+
const keyName = s.names[0];
|
|
692
|
+
const keyTy = s.nameTypes[0] ?? s.iterable.ty.key ?? { kind: "unknown" };
|
|
693
|
+
const count = _forofCounters.get(keyName) ?? 0;
|
|
694
|
+
_forofCounters.set(keyName, count + 1);
|
|
695
|
+
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
696
|
+
const keysSeqName = `_${keyName}_keys${suffix}`;
|
|
697
|
+
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
698
|
+
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
699
|
+
const keysVar = { kind: "var", name: keysSeqName };
|
|
700
|
+
const idxName = `_${keyName}_idx${suffix}`;
|
|
701
|
+
const idx = { kind: "var", name: idxName };
|
|
702
|
+
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
703
|
+
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
704
|
+
const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
|
|
705
|
+
const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
|
|
706
|
+
result.push({
|
|
707
|
+
kind: "forin", idx: idxName, bound: arrSize,
|
|
708
|
+
invariants: [boundInv, ...s.invariants.map(transformExpr)],
|
|
709
|
+
body: [letKey, ...bodyStmts],
|
|
710
|
+
});
|
|
711
|
+
i++;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
640
714
|
// Map iteration: for (const [k, v] of map) → iterate keys, look up values
|
|
641
715
|
if (s.names.length >= 2 && s.iterable.ty.kind === "map") {
|
|
642
716
|
const keyName = s.names[0], valueName = s.names[1];
|
|
643
717
|
const keyTy = s.nameTypes[0] ?? { kind: "unknown" };
|
|
644
718
|
const valueTy = s.nameTypes[1] ?? { kind: "unknown" };
|
|
645
|
-
const keysSeqName = `_${keyName}_keys`;
|
|
646
|
-
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
647
|
-
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
648
|
-
const keysVar = { kind: "var", name: keysSeqName };
|
|
649
719
|
const count = _forofCounters.get(keyName) ?? 0;
|
|
650
720
|
_forofCounters.set(keyName, count + 1);
|
|
651
721
|
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
722
|
+
const keysSeqName = `_${keyName}_keys${suffix}`;
|
|
723
|
+
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
724
|
+
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
725
|
+
const keysVar = { kind: "var", name: keysSeqName };
|
|
652
726
|
const idxName = `_${keyName}_idx${suffix}`;
|
|
653
727
|
const idx = { kind: "var", name: idxName };
|
|
654
728
|
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
@@ -770,6 +844,21 @@ function transformStmt(s, typeDecls) {
|
|
|
770
844
|
return stmts;
|
|
771
845
|
}
|
|
772
846
|
}
|
|
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
|
+
}
|
|
773
862
|
const { binds, expr } = liftMethodCalls(s.init);
|
|
774
863
|
return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
|
|
775
864
|
}
|
|
@@ -917,7 +1006,8 @@ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts, fieldExpr)
|
|
|
917
1006
|
if (someBranch.length === 0 && restStmts && restStmts.length > 0) {
|
|
918
1007
|
someBranch = restStmts;
|
|
919
1008
|
}
|
|
920
|
-
const
|
|
1009
|
+
const sanitized = varName.replace(/\./g, "_");
|
|
1010
|
+
const bound = matchBinder(`${sanitized}_val`);
|
|
921
1011
|
// Replace the narrowed variable/field in the Some branch body.
|
|
922
1012
|
// Field chains: replace in TStmt before transform (so downstream narrowing sees simple vars).
|
|
923
1013
|
// Simple vars: replace in IR after transform (the original mechanism).
|
|
@@ -931,10 +1021,14 @@ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts, fieldExpr)
|
|
|
931
1021
|
}
|
|
932
1022
|
else {
|
|
933
1023
|
const transformed = transformStmts(someBranch, typeDecls);
|
|
934
|
-
someBody = transformed.map(stmt => mapStmtExprs(stmt, e => replaceVar(e, varName, { kind: "var", name: bound })));
|
|
1024
|
+
someBody = transformed.map(stmt => mapStmtExprs(stmt, e => replaceVar(e, varName, { kind: "var", name: bound }, true)));
|
|
935
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;
|
|
936
1030
|
return {
|
|
937
|
-
kind: "match", scrutinee
|
|
1031
|
+
kind: "match", scrutinee,
|
|
938
1032
|
arms: [
|
|
939
1033
|
{ pattern: `.some ${bound}`, body: someBody },
|
|
940
1034
|
{ pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
|
|
@@ -947,25 +1041,21 @@ function mapStmtExprs(s, r) {
|
|
|
947
1041
|
}
|
|
948
1042
|
// ── Optional narrowing helpers ──────────────────────────────
|
|
949
1043
|
//
|
|
950
|
-
// Optional narrowing converts TS `if (x === undefined)`
|
|
951
|
-
// `match x { Some(val) => ..., None => ... }`.
|
|
1044
|
+
// Optional narrowing converts TS `if (x === undefined)` / `x !== undefined ? a : b`
|
|
1045
|
+
// patterns to `match x { Some(val) => ..., None => ... }`.
|
|
952
1046
|
//
|
|
953
|
-
// The resolve phase (resolve.ts) handles:
|
|
1047
|
+
// The resolve phase (resolve.ts) handles TYPE narrowing only:
|
|
954
1048
|
// - Flow narrowing: after `if (x === undefined) return`, x is non-optional
|
|
955
1049
|
// - && narrowing: in `x !== undefined && f(x)`, f(x) sees x as non-optional
|
|
956
|
-
// - Conditional narrowing:
|
|
957
|
-
//
|
|
1050
|
+
// - Conditional type narrowing: extends env (simple vars) or narrowedFields context
|
|
1051
|
+
// (field chains) so the then-branch resolves with the unwrapped type
|
|
958
1052
|
//
|
|
959
|
-
// The transform phase (here) handles:
|
|
1053
|
+
// The transform phase (here) handles ALL structural narrowing:
|
|
960
1054
|
// - Statement-level: `transformStmts` detects optional checks → `emitOptionalMatch`
|
|
961
|
-
// - Expression-level: `lowerExpr`
|
|
962
|
-
//
|
|
963
|
-
//
|
|
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.
|
|
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 }`
|
|
969
1059
|
/** Shared logic for optional match in both imperative and pure function paths.
|
|
970
1060
|
* Detects optional check, selects branches, handles early-return consumption.
|
|
971
1061
|
* Returns null if the condition is not an optional check. */
|
|
@@ -978,7 +1068,8 @@ function prepareOptionalMatch(s, restStmts) {
|
|
|
978
1068
|
// Early-return pattern: Some branch is empty → consume rest of block
|
|
979
1069
|
if (someBranch.length === 0 && restStmts.length > 0)
|
|
980
1070
|
someBranch = restStmts;
|
|
981
|
-
const
|
|
1071
|
+
const sanitized = check.varName.replace(/\./g, "_");
|
|
1072
|
+
const bound = matchBinder(`${sanitized}_val`);
|
|
982
1073
|
return { check, someBranch, noneBranch, bound };
|
|
983
1074
|
}
|
|
984
1075
|
/** Extract the leftmost optional check from a && chain, returning the check and the rest.
|
|
@@ -1092,6 +1183,96 @@ function replaceFieldAccessInTStmts(stmts, varName, fields) {
|
|
|
1092
1183
|
fallbackTy: f.type ?? parseTsType(f.tsType),
|
|
1093
1184
|
})));
|
|
1094
1185
|
}
|
|
1186
|
+
/** Replace obj.field → replacement var in typed IR expressions (before lowering).
|
|
1187
|
+
* Mirrors replaceFieldsInTStmts but operates on a single TExpr tree. */
|
|
1188
|
+
function replaceFieldInTExpr(expr, objName, replacements) {
|
|
1189
|
+
if (replacements.length === 0)
|
|
1190
|
+
return expr;
|
|
1191
|
+
return mapTExpr(expr, e => {
|
|
1192
|
+
if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === objName) {
|
|
1193
|
+
const r = replacements.find(r => r.fieldName === e.field);
|
|
1194
|
+
if (r) {
|
|
1195
|
+
const ty = e.ty.kind !== "unknown" ? e.ty : r.fallbackTy;
|
|
1196
|
+
return { kind: "var", name: r.newName, ty };
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return null;
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
/** Unwrap optional type on match-bound variables in TExpr.
|
|
1203
|
+
* After replaceFieldInTExpr, the replaced variable carries the original optional
|
|
1204
|
+
* type from the field declaration. The match binding unwraps it to the inner type. */
|
|
1205
|
+
function fixBoundType(expr, boundName) {
|
|
1206
|
+
return mapTExpr(expr, e => e.kind === "var" && e.name === boundName && e.ty.kind === "optional"
|
|
1207
|
+
? { ...e, ty: e.ty.inner } : null);
|
|
1208
|
+
}
|
|
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
|
+
}
|
|
1095
1276
|
// ── Pure function generation ─────────────────────────────────
|
|
1096
1277
|
function transformPureBody(stmts, typeDecls) {
|
|
1097
1278
|
// Detect discriminant if-chain
|
|
@@ -1112,16 +1293,42 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1112
1293
|
return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
|
|
1113
1294
|
}
|
|
1114
1295
|
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
|
+
}
|
|
1115
1306
|
// Optional narrowing: if (x === undefined) → match x { None => ..., Some(x_val) => ... }
|
|
1116
1307
|
const optMatch = prepareOptionalMatch(s, rest);
|
|
1117
1308
|
if (optMatch) {
|
|
1118
|
-
|
|
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);
|
|
1119
1323
|
if (!someExpr)
|
|
1120
1324
|
return null;
|
|
1121
1325
|
const noneExpr = transformPureBody(optMatch.noneBranch, typeDecls);
|
|
1122
1326
|
if (!noneExpr)
|
|
1123
1327
|
return null;
|
|
1124
|
-
|
|
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);
|
|
1125
1332
|
return {
|
|
1126
1333
|
kind: "match", scrutinee: optMatch.check.varName,
|
|
1127
1334
|
arms: [
|
|
@@ -1130,11 +1337,13 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1130
1337
|
],
|
|
1131
1338
|
};
|
|
1132
1339
|
}
|
|
1133
|
-
|
|
1340
|
+
// Append rest to both branches so nested ifs that fall through
|
|
1341
|
+
// can reach the continuation (e.g. early return inside then-branch)
|
|
1342
|
+
const thenExpr = transformPureBody([...s.then, ...rest], typeDecls);
|
|
1134
1343
|
if (!thenExpr)
|
|
1135
1344
|
return null;
|
|
1136
|
-
const
|
|
1137
|
-
const elseExpr = transformPureBody(
|
|
1345
|
+
const elseStmts = s.else.length > 0 ? [...s.else, ...rest] : rest;
|
|
1346
|
+
const elseExpr = transformPureBody(elseStmts, typeDecls);
|
|
1138
1347
|
if (!elseExpr)
|
|
1139
1348
|
return null;
|
|
1140
1349
|
return { kind: "if", cond: transformExpr(s.cond), then: thenExpr, else: elseExpr };
|
|
@@ -1265,17 +1474,38 @@ function findReassignedNames(stmts, names) {
|
|
|
1265
1474
|
return found;
|
|
1266
1475
|
}
|
|
1267
1476
|
/** Replace all occurrences of a variable name with a new expression. */
|
|
1268
|
-
|
|
1477
|
+
/**
|
|
1478
|
+
* Replace all occurrences of variable `name` with `replacement`.
|
|
1479
|
+
* If `narrowing` is true, the replacement is an unwrapped Optional value
|
|
1480
|
+
* (e.g., replacing `x: Option<T>` with `x_val: T`). In that case, when the
|
|
1481
|
+
* variable appears directly as a record spread field value, it's wrapped in
|
|
1482
|
+
* Some() to preserve the field's Optional type.
|
|
1483
|
+
*/
|
|
1484
|
+
function replaceVar(e, name, replacement, narrowing) {
|
|
1485
|
+
const rec = (expr) => replaceVar(expr, name, replacement, narrowing);
|
|
1269
1486
|
return mapExpr(e, x => {
|
|
1270
1487
|
if (x.kind === "var" && x.name === name)
|
|
1271
1488
|
return replacement;
|
|
1489
|
+
// Record spread: wrap direct variable uses in field values with Some when narrowing
|
|
1490
|
+
if (narrowing && x.kind === "record" && x.spread) {
|
|
1491
|
+
return {
|
|
1492
|
+
...x,
|
|
1493
|
+
spread: rec(x.spread),
|
|
1494
|
+
fields: x.fields.map(f => {
|
|
1495
|
+
if (f.value.kind === "var" && f.value.name === name) {
|
|
1496
|
+
return { ...f, value: { kind: "app", fn: "Some", args: [replacement] } };
|
|
1497
|
+
}
|
|
1498
|
+
return { ...f, value: rec(f.value) };
|
|
1499
|
+
}),
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1272
1502
|
// Don't descend past bindings that shadow the name
|
|
1273
1503
|
if (x.kind === "forall" && x.var === name)
|
|
1274
1504
|
return x;
|
|
1275
1505
|
if (x.kind === "exists" && x.var === name)
|
|
1276
1506
|
return x;
|
|
1277
1507
|
if (x.kind === "let" && x.name === name)
|
|
1278
|
-
return { ...x, value: replaceVar(x.value, name, replacement) };
|
|
1508
|
+
return { ...x, value: replaceVar(x.value, name, replacement, narrowing) };
|
|
1279
1509
|
return null;
|
|
1280
1510
|
});
|
|
1281
1511
|
}
|
|
@@ -1316,25 +1546,43 @@ export function transformModule(mod, specImport) {
|
|
|
1316
1546
|
}));
|
|
1317
1547
|
// Pure function mirrors
|
|
1318
1548
|
const pureDefs = [];
|
|
1549
|
+
const defByMethods = [];
|
|
1319
1550
|
for (const fn of mod.functions) {
|
|
1320
1551
|
if (!fn.isPure)
|
|
1321
1552
|
continue;
|
|
1322
1553
|
const body = transformPureBody(fn.body, mod.typeDecls);
|
|
1323
|
-
if (
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1554
|
+
if (body) {
|
|
1555
|
+
// For ensures, replace \result (→ "res") with the function call
|
|
1556
|
+
const fnCall = { kind: "app", fn: fn.name, args: fn.params.map(p => ({ kind: "var", name: p.name })) };
|
|
1557
|
+
const ensures = fn.ensures.map(e => replaceVar(transformExpr(e), "res", fnCall));
|
|
1558
|
+
pureDefs.push({
|
|
1559
|
+
kind: "def",
|
|
1560
|
+
name: fn.name,
|
|
1561
|
+
typeParams: fn.typeParams,
|
|
1562
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1563
|
+
returnType: fn.returnTy,
|
|
1564
|
+
requires: fn.requires.map(transformExpr),
|
|
1565
|
+
ensures,
|
|
1566
|
+
decreases: fn.decreases ? transformExpr(fn.decreases) : null,
|
|
1567
|
+
body,
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
else if (fn.forcePure) {
|
|
1571
|
+
// //@ pure but body can't be auto-converted — emit function by method
|
|
1572
|
+
_forofCounters.clear();
|
|
1573
|
+
const methodBody = transformStmts(fn.body, mod.typeDecls);
|
|
1574
|
+
defByMethods.push({
|
|
1575
|
+
kind: "def-by-method",
|
|
1576
|
+
name: fn.name,
|
|
1577
|
+
typeParams: fn.typeParams,
|
|
1578
|
+
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1579
|
+
returnType: fn.returnTy,
|
|
1580
|
+
requires: fn.requires.map(transformExpr),
|
|
1581
|
+
ensures: fn.ensures.map(transformExpr),
|
|
1582
|
+
decreases: fn.decreases ? transformExpr(fn.decreases) : null,
|
|
1583
|
+
methodBody,
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1338
1586
|
}
|
|
1339
1587
|
const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
|
|
1340
1588
|
// Types file
|
|
@@ -1353,7 +1601,8 @@ export function transformModule(mod, specImport) {
|
|
|
1353
1601
|
}
|
|
1354
1602
|
// Def file: Velvet methods
|
|
1355
1603
|
// Pure functions get a thin wrapper that calls Pure.fnName
|
|
1356
|
-
|
|
1604
|
+
// def-by-method functions also skip their method wrappers
|
|
1605
|
+
const pureDefNames = new Set([...pureDefs.map(d => d.name), ...defByMethods.map(d => d.name)]);
|
|
1357
1606
|
const methods = mod.functions.map(fn => {
|
|
1358
1607
|
const ensures = [];
|
|
1359
1608
|
for (const e of fn.ensures) {
|
|
@@ -1420,7 +1669,7 @@ export function transformModule(mod, specImport) {
|
|
|
1420
1669
|
{ key: "loom.semantics.termination", value: '"total"' },
|
|
1421
1670
|
{ key: "loom.semantics.choice", value: '"demonic"' },
|
|
1422
1671
|
],
|
|
1423
|
-
decls: [...constDecls, ...methods, ...classDecls],
|
|
1672
|
+
decls: [...constDecls, ...defByMethods, ...methods, ...classDecls],
|
|
1424
1673
|
};
|
|
1425
1674
|
return { typesFile, defFile };
|
|
1426
1675
|
}
|