lemmascript 0.5.7 → 0.5.9
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 -1
- package/package.json +1 -1
- package/tools/dist/dafny-emit.js +50 -4
- package/tools/dist/extract.js +106 -39
- package/tools/dist/ir.js +56 -1
- package/tools/dist/lean-emit.js +242 -22
- package/tools/dist/lsc.js +14 -6
- package/tools/dist/narrow.js +89 -2
- package/tools/dist/peephole.js +2 -0
- package/tools/dist/resolve.js +147 -5
- package/tools/dist/transform.js +227 -9
- package/tools/dist/types.js +14 -0
- package/tools/dist/guard-command.js +0 -238
package/tools/dist/resolve.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* No mutation — each let extends the chain, lookup walks it.
|
|
6
6
|
*/
|
|
7
7
|
import { isBigInt } from "./typedir.js";
|
|
8
|
-
import { parseTsType } from "./types.js";
|
|
8
|
+
import { parseTsType, tyToCanonical } from "./types.js";
|
|
9
9
|
import { parseExpr } from "./specparser.js";
|
|
10
10
|
function lookup(env, name) {
|
|
11
11
|
if (!env)
|
|
@@ -349,6 +349,14 @@ function isUnmodeledTy(ty, typeDecls) {
|
|
|
349
349
|
}
|
|
350
350
|
return false;
|
|
351
351
|
}
|
|
352
|
+
/** A `user` type that resolves to a string-union declare-type — runs as a plain
|
|
353
|
+
* string at runtime, so it's a refinement of `string`, not an opaque blob. */
|
|
354
|
+
function isStringUnionTy(ty, typeDecls) {
|
|
355
|
+
if (ty.kind !== "user")
|
|
356
|
+
return false;
|
|
357
|
+
const base = ty.name.includes("<") ? ty.name.slice(0, ty.name.indexOf("<")) : ty.name;
|
|
358
|
+
return typeDecls.some(d => d.name === base && d.kind === "string-union");
|
|
359
|
+
}
|
|
352
360
|
/** Infer quantifier variable type from usage in body.
|
|
353
361
|
* If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
|
|
354
362
|
* return the collection's key type. Otherwise return null (default to int). */
|
|
@@ -457,6 +465,16 @@ function tyToTsStr(ty) {
|
|
|
457
465
|
return undefined;
|
|
458
466
|
}
|
|
459
467
|
function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
468
|
+
// sort's comparator takes two params, both the element type.
|
|
469
|
+
if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "sort" &&
|
|
470
|
+
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" && rawArgs[0].params.length >= 1) {
|
|
471
|
+
const tsType = tyToTsStr(fn.obj.ty.elem);
|
|
472
|
+
if (tsType) {
|
|
473
|
+
const lam = rawArgs[0];
|
|
474
|
+
const updatedParams = lam.params.map(p => (p.tsType ? p : { ...p, tsType }));
|
|
475
|
+
return [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
460
478
|
if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
461
479
|
["map", "filter", "every", "some", "find", "findLast", "findIndex"].includes(fn.field) &&
|
|
462
480
|
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
|
|
@@ -573,7 +591,9 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
573
591
|
return objTy.elem;
|
|
574
592
|
if (fn.field === "pop")
|
|
575
593
|
return { kind: "optional", inner: objTy.elem };
|
|
576
|
-
if (fn.field === "push" || fn.field === "concat")
|
|
594
|
+
if (fn.field === "push" || fn.field === "unshift" || fn.field === "concat")
|
|
595
|
+
return objTy;
|
|
596
|
+
if (fn.field === "sort")
|
|
577
597
|
return objTy;
|
|
578
598
|
if (fn.field === "filter")
|
|
579
599
|
return objTy;
|
|
@@ -639,6 +659,106 @@ function lookupFieldTy(objTy, field, ctx) {
|
|
|
639
659
|
return { ty: { kind: "unknown" }, isDiscriminant: false };
|
|
640
660
|
}
|
|
641
661
|
// ── Resolve expressions ──────────────────────────────────────
|
|
662
|
+
// Fresh-binder counter for the someMatches synthesized by object-spread merge.
|
|
663
|
+
let mergeBinder = 0;
|
|
664
|
+
/** Expand `{ ...base, ...override }` into a faithful field-wise merge. Driven by
|
|
665
|
+
* the result record type's fields: an override field wins when present, else the
|
|
666
|
+
* base's field shows through. Optional fields decide presence at runtime
|
|
667
|
+
* (`Some?`); an `Option`-typed override is the whole merge guarded by its tag. */
|
|
668
|
+
function resolveRecordMerge(base, override, ctx) {
|
|
669
|
+
const tbase = resolveExpr(base, ctx);
|
|
670
|
+
const tover = resolveExpr(override, ctx);
|
|
671
|
+
const overInner = tover.ty.kind === "optional" ? tover.ty.inner : tover.ty;
|
|
672
|
+
// Result record type: prefer the override's (an optional override still merges
|
|
673
|
+
// into its inner type), else the base's.
|
|
674
|
+
const rTy = overInner.kind === "user" ? overInner
|
|
675
|
+
: tbase.ty.kind === "user" ? tbase.ty : null;
|
|
676
|
+
const decl = rTy ? ctx.typeDecls.find(d => d.name === rTy.name && d.kind === "record") : undefined;
|
|
677
|
+
if (!rTy || !decl?.fields) {
|
|
678
|
+
throw new Error(`object spread merge { ...a, ...b } needs a known record type for both operands ` +
|
|
679
|
+
`(base: ${tyToCanonical(tbase.ty)}, override: ${tyToCanonical(tover.ty)})`);
|
|
680
|
+
}
|
|
681
|
+
if (tbase.ty.kind === "optional") {
|
|
682
|
+
throw new Error(`object spread merge with an optional base operand is not supported (base: ${tyToCanonical(tbase.ty)})`);
|
|
683
|
+
}
|
|
684
|
+
const userTy = rTy;
|
|
685
|
+
const fields = decl.fields;
|
|
686
|
+
// Build the merged literal from concrete base/override values, both : userTy.
|
|
687
|
+
const merged = (bv, ov) => ({
|
|
688
|
+
kind: "record", spread: null, ty: userTy,
|
|
689
|
+
fields: fields.map(f => {
|
|
690
|
+
const ft = f.type;
|
|
691
|
+
const ovf = { kind: "field", obj: ov, field: f.name, ty: ft };
|
|
692
|
+
if (ft.kind !== "optional")
|
|
693
|
+
return { name: f.name, value: ovf }; // required: override always provides
|
|
694
|
+
// optional: override field wins iff present, else base's field
|
|
695
|
+
const bvf = { kind: "field", obj: bv, field: f.name, ty: ft };
|
|
696
|
+
const binder = `_m${mergeBinder++}`;
|
|
697
|
+
// someBody is the unwrapped present value; transform re-wraps each arm in
|
|
698
|
+
// the backend's Some constructor (Dafny `Some`, Lean `Option.some`).
|
|
699
|
+
return { name: f.name, value: {
|
|
700
|
+
kind: "someMatch", scrutinee: ovf, binder, binderTy: ft.inner,
|
|
701
|
+
someBody: { kind: "var", name: binder, ty: ft.inner }, noneBody: bvf, ty: ft,
|
|
702
|
+
} };
|
|
703
|
+
}),
|
|
704
|
+
});
|
|
705
|
+
if (tover.ty.kind === "optional") {
|
|
706
|
+
// override may be absent (undefined spreads nothing) → base unchanged
|
|
707
|
+
const binder = `_mo${mergeBinder++}`;
|
|
708
|
+
return {
|
|
709
|
+
kind: "someMatch", scrutinee: tover, binder, binderTy: userTy,
|
|
710
|
+
someBody: merged(tbase, { kind: "var", name: binder, ty: userTy }), noneBody: tbase, ty: userTy,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
return merged(tbase, tover);
|
|
714
|
+
}
|
|
715
|
+
/** `rec[k]` where `rec` is a record and `k` an enum of its field names. A *named*
|
|
716
|
+
* string-union key is a datatype → `match k { case f => rec.f }`; an *inline*
|
|
717
|
+
* union (`"a" | "b"`, a bare string carrying its members) → an equality chain
|
|
718
|
+
* `if k === "a" then rec.a else …`. Either way the chain/match covers exactly
|
|
719
|
+
* the key's values, so a subset key stays sound. Returns null if the shape
|
|
720
|
+
* doesn't apply (caller falls back to plain index). */
|
|
721
|
+
function tryRecordIndexByEnum(obj, idx, ctx) {
|
|
722
|
+
const objTy = obj.ty, keyTy = idx.ty;
|
|
723
|
+
if (objTy.kind !== "user")
|
|
724
|
+
return null;
|
|
725
|
+
const rec = ctx.typeDecls.find(d => d.name === objTy.name && d.kind === "record");
|
|
726
|
+
if (!rec?.fields)
|
|
727
|
+
return null;
|
|
728
|
+
const fieldByName = new Map(rec.fields.map(f => [f.name, f]));
|
|
729
|
+
const fieldTy = (v) => fieldByName.get(v).type ?? { kind: "unknown" };
|
|
730
|
+
const field = (v) => ({ kind: "field", obj, field: v, ty: fieldTy(v) });
|
|
731
|
+
// The key's members, and whether it's a datatype (named) or a bare string (inline).
|
|
732
|
+
let values = null;
|
|
733
|
+
let datatype = null;
|
|
734
|
+
if (keyTy.kind === "user") {
|
|
735
|
+
const keyEnum = ctx.typeDecls.find(d => d.name === keyTy.name && d.kind === "string-union");
|
|
736
|
+
if (keyEnum?.values?.length) {
|
|
737
|
+
values = keyEnum.values;
|
|
738
|
+
datatype = keyEnum.name;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
else if (keyTy.kind === "string" && keyTy.values?.length) {
|
|
742
|
+
values = keyTy.values;
|
|
743
|
+
}
|
|
744
|
+
if (!values || !values.every(v => fieldByName.has(v)))
|
|
745
|
+
return null; // key isn't a subset of fields
|
|
746
|
+
if (datatype) {
|
|
747
|
+
return {
|
|
748
|
+
kind: "tagMatch", scrutinee: idx, typeName: datatype,
|
|
749
|
+
cases: values.map(v => ({ variant: v, body: field(v) })), fallthrough: null, ty: fieldTy(values[0]),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
// Inline union: fold right into an equality chain, last member as the bare else.
|
|
753
|
+
let expr = field(values[values.length - 1]);
|
|
754
|
+
for (let i = values.length - 2; i >= 0; i--) {
|
|
755
|
+
expr = {
|
|
756
|
+
kind: "conditional", ty: fieldTy(values[i]), then: field(values[i]), else: expr,
|
|
757
|
+
cond: { kind: "binop", op: "===", left: idx, right: { kind: "str", value: values[i], ty: { kind: "string" } }, ty: { kind: "bool" } },
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return expr;
|
|
761
|
+
}
|
|
642
762
|
function resolveExpr(e, ctx) {
|
|
643
763
|
switch (e.kind) {
|
|
644
764
|
case "var":
|
|
@@ -812,6 +932,9 @@ function resolveExpr(e, ctx) {
|
|
|
812
932
|
idxTy = narrowed ? obj.ty.value : { kind: "optional", inner: obj.ty.value };
|
|
813
933
|
}
|
|
814
934
|
else {
|
|
935
|
+
const recIdx = tryRecordIndexByEnum(obj, idx, ctx);
|
|
936
|
+
if (recIdx)
|
|
937
|
+
return recIdx;
|
|
815
938
|
idxTy = { kind: "unknown" };
|
|
816
939
|
}
|
|
817
940
|
return { kind: "index", obj, idx, ty: idxTy };
|
|
@@ -842,8 +965,10 @@ function resolveExpr(e, ctx) {
|
|
|
842
965
|
// left ?? right — result type is left's inner (when left is optional)
|
|
843
966
|
// or just left's type, unified with right's type.
|
|
844
967
|
const left = resolveExpr(e.left, ctx);
|
|
845
|
-
const right = resolveExpr(e.right, ctx);
|
|
846
968
|
const ty = left.ty.kind === "optional" ? left.ty.inner : left.ty;
|
|
969
|
+
// The default shares the result type, so coerce a string literal to a
|
|
970
|
+
// string-union enum (e.g. `availableLevels[0] ?? "off"`).
|
|
971
|
+
const right = coerceStr(resolveExpr(e.right, ctx), ty);
|
|
847
972
|
return { kind: "nullish", left, right, ty };
|
|
848
973
|
}
|
|
849
974
|
case "optChain": {
|
|
@@ -944,6 +1069,8 @@ function resolveExpr(e, ctx) {
|
|
|
944
1069
|
});
|
|
945
1070
|
return { kind: "record", spread, fields, ty: recordTy ?? ty };
|
|
946
1071
|
}
|
|
1072
|
+
case "recordMerge":
|
|
1073
|
+
return resolveRecordMerge(e.base, e.override, ctx);
|
|
947
1074
|
case "result":
|
|
948
1075
|
// \result desugars to a regular var so all the variable-narrowing
|
|
949
1076
|
// machinery (env lookup, optional checks, path matching) just works.
|
|
@@ -967,8 +1094,14 @@ function resolveExpr(e, ctx) {
|
|
|
967
1094
|
// anonymous tuple (mirrors return-position and call-argument records, which
|
|
968
1095
|
// get their type via ctx.returnTy). Only narrow when the context type is an
|
|
969
1096
|
// array; otherwise leave ctx untouched.
|
|
970
|
-
const
|
|
971
|
-
const
|
|
1097
|
+
const expectedElem = ctx.returnTy.kind === "array" ? ctx.returnTy.elem : null;
|
|
1098
|
+
const elemCtx = expectedElem ? { ...ctx, returnTy: expectedElem } : ctx;
|
|
1099
|
+
const elems = e.elems.map(el => {
|
|
1100
|
+
const r = resolveExpr(el, elemCtx);
|
|
1101
|
+
// Coerce a bare string-literal element to a string-union enum (e.g.
|
|
1102
|
+
// `["off", …]: ModelThinkingLevel[]`), like return/arg positions.
|
|
1103
|
+
return expectedElem ? coerceStr(r, expectedElem) : r;
|
|
1104
|
+
});
|
|
972
1105
|
const elemTy = elems.length > 0 ? elems[0].ty : { kind: "unknown" };
|
|
973
1106
|
return { kind: "arrayLiteral", elems, ty: { kind: "array", elem: elemTy } };
|
|
974
1107
|
}
|
|
@@ -1140,6 +1273,11 @@ function resolveStmt(s, ctx) {
|
|
|
1140
1273
|
? { kind: "optional", inner: init.ty }
|
|
1141
1274
|
: init.ty;
|
|
1142
1275
|
}
|
|
1276
|
+
else if (declTy.kind === "string" && isStringUnionTy(init.ty, ctx.typeDecls) && !ctx.overrides.has(s.name)) {
|
|
1277
|
+
// ts-morph widened a string-union to `string`; keep the initializer's
|
|
1278
|
+
// datatype so `local === "lit"` lowers to a discriminant test.
|
|
1279
|
+
ty = init.ty;
|
|
1280
|
+
}
|
|
1143
1281
|
else if ((declTy.kind === "int" || declTy.kind === "nat") && init.ty.kind === "real" && !ctx.overrides.has(s.name)) {
|
|
1144
1282
|
// TS infers `number` (→ int/nat) for an expression LS computes as `real`
|
|
1145
1283
|
// (e.g. `a / b`, now real division). `number` can't tell them apart, so
|
|
@@ -1332,6 +1470,10 @@ function collectCallsExpr(e, fns, out) {
|
|
|
1332
1470
|
for (const f of e.fields)
|
|
1333
1471
|
collectCallsExpr(f.value, fns, out);
|
|
1334
1472
|
return;
|
|
1473
|
+
case "recordMerge":
|
|
1474
|
+
collectCallsExpr(e.base, fns, out);
|
|
1475
|
+
collectCallsExpr(e.override, fns, out);
|
|
1476
|
+
return;
|
|
1335
1477
|
case "arrayLiteral":
|
|
1336
1478
|
for (const el of e.elems)
|
|
1337
1479
|
collectCallsExpr(el, fns, out);
|
package/tools/dist/transform.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Consumes resolved types and classifications.
|
|
5
5
|
* No type lookups, no string parsing, no re-inference.
|
|
6
6
|
*/
|
|
7
|
+
import { anyExprInStmts } from "./ir.js";
|
|
7
8
|
import { parseTsType } from "./types.js";
|
|
8
9
|
// ── Generic IR walkers ──────────────────────────────────────
|
|
9
10
|
/**
|
|
@@ -23,7 +24,8 @@ function mapExpr(e, f) {
|
|
|
23
24
|
case "str":
|
|
24
25
|
case "emptyMap":
|
|
25
26
|
case "emptySet":
|
|
26
|
-
case "havoc":
|
|
27
|
+
case "havoc":
|
|
28
|
+
case "default": return e;
|
|
27
29
|
case "mapLiteral": return { ...e, entries: e.entries.map(en => ({ key: r(en.key), value: r(en.value) })) };
|
|
28
30
|
case "constructor": return e.args ? { ...e, args: e.args.map(r) } : e;
|
|
29
31
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
@@ -630,6 +632,17 @@ function lowerExpr(e, binds) {
|
|
|
630
632
|
return { kind: "field", obj: transformExpr(e.obj), field: "true_?" };
|
|
631
633
|
}
|
|
632
634
|
}
|
|
635
|
+
// Union destructor: `x.field` where x is a discriminated union and `field`
|
|
636
|
+
// is a data field of one of its variants. Dafny reads the destructor
|
|
637
|
+
// directly; Lean has no field projection on a multi-ctor inductive, so tag
|
|
638
|
+
// the node with the union's base name and let the Lean emitter `match`.
|
|
639
|
+
if (e.obj.ty.kind === "user") {
|
|
640
|
+
const baseName = e.obj.ty.name.includes("<") ? e.obj.ty.name.slice(0, e.obj.ty.name.indexOf("<")) : e.obj.ty.name;
|
|
641
|
+
const decl = _typeDecls.find(d => d.name === baseName && d.kind === "discriminated-union");
|
|
642
|
+
if (decl?.variants?.some(v => v.fields.some(f => f.name === e.field))) {
|
|
643
|
+
return { kind: "field", obj: transformExpr(e.obj), field: e.field, fromUnion: baseName };
|
|
644
|
+
}
|
|
645
|
+
}
|
|
633
646
|
return { kind: "field", obj: transformExpr(e.obj), field: e.field };
|
|
634
647
|
case "index": {
|
|
635
648
|
const idx = transformExpr(e.idx);
|
|
@@ -1106,6 +1119,188 @@ function buildPopLowering(arrName, arrTy) {
|
|
|
1106
1119
|
const guardedTrunc = { kind: "if", cond: isNonEmpty, then: truncated, else: arrVar };
|
|
1107
1120
|
return { optValue, guardedTrunc };
|
|
1108
1121
|
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Velvet (Lean backend) rejects `return` inside a loop. For a function shaped as
|
|
1124
|
+
* `…; while (…) { … early returns … }; return fallthrough`, hoist a mutable
|
|
1125
|
+
* result variable: each in-loop `return e` becomes `_loopRet := e; break`, then
|
|
1126
|
+
* `return _loopRet` after the loop. The trailing fallthrough return seeds
|
|
1127
|
+
* `_loopRet`, so a normal loop exit returns it unchanged. Dafny is untouched —
|
|
1128
|
+
* it keeps the native early returns.
|
|
1129
|
+
*/
|
|
1130
|
+
function stmtsContainReturn(stmts) {
|
|
1131
|
+
for (const s of stmts) {
|
|
1132
|
+
if (s.kind === "return")
|
|
1133
|
+
return true;
|
|
1134
|
+
if (s.kind === "if" && (stmtsContainReturn(s.then) || stmtsContainReturn(s.else)))
|
|
1135
|
+
return true;
|
|
1136
|
+
if (s.kind === "match" && s.arms.some(a => stmtsContainReturn(a.body)))
|
|
1137
|
+
return true;
|
|
1138
|
+
// Don't descend into a nested while — its returns would target that loop.
|
|
1139
|
+
}
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
function replaceReturnsWithBreak(stmts, retVar) {
|
|
1143
|
+
return stmts.flatMap((s) => {
|
|
1144
|
+
if (s.kind === "return")
|
|
1145
|
+
return [{ kind: "assign", target: retVar, value: s.value }, { kind: "break" }];
|
|
1146
|
+
if (s.kind === "if")
|
|
1147
|
+
return [{ ...s, then: replaceReturnsWithBreak(s.then, retVar), else: replaceReturnsWithBreak(s.else, retVar) }];
|
|
1148
|
+
if (s.kind === "match")
|
|
1149
|
+
return [{ ...s, arms: s.arms.map(a => ({ ...a, body: replaceReturnsWithBreak(a.body, retVar) })) }];
|
|
1150
|
+
return [s];
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
// Seed value for `_loopRet` when a return-in-loop function has no trailing
|
|
1154
|
+
// fallthrough return. Readable literals for the primitives; everything else
|
|
1155
|
+
// (user types, arrays, maps, optionals) gets a typed `default : T` rather than a
|
|
1156
|
+
// type-incorrect `0` — these types derive `Inhabited`, so the default exists.
|
|
1157
|
+
function defaultExprForTy(ty) {
|
|
1158
|
+
switch (ty.kind) {
|
|
1159
|
+
case "bool": return { kind: "bool", value: false };
|
|
1160
|
+
case "string": return { kind: "str", value: "" };
|
|
1161
|
+
case "nat":
|
|
1162
|
+
case "int":
|
|
1163
|
+
case "real": return { kind: "num", value: 0 };
|
|
1164
|
+
default: return { kind: "default", type: ty };
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function eliminateReturnInLoops(stmts, retTy, resultInvariants) {
|
|
1168
|
+
const out = [];
|
|
1169
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
1170
|
+
const s = stmts[i];
|
|
1171
|
+
if (s.kind === "while" && stmtsContainReturn(s.body)) {
|
|
1172
|
+
const retVar = "_loopRet";
|
|
1173
|
+
const next = stmts[i + 1];
|
|
1174
|
+
const init = next && next.kind === "return" ? next.value : defaultExprForTy(retTy);
|
|
1175
|
+
out.push({ kind: "let", name: retVar, type: retTy, mutable: true, value: init });
|
|
1176
|
+
// The result variable carries the postcondition (the function's `ensures`
|
|
1177
|
+
// with `\result` → `_loopRet`) as a loop invariant: it holds initially (the
|
|
1178
|
+
// fallthrough seed) and after each `_loopRet := e; break`, so the
|
|
1179
|
+
// postcondition is re-established from the invariant when the loop exits.
|
|
1180
|
+
// Where Dafny discharged the postcondition at each `return` site, the
|
|
1181
|
+
// break-rewrite discharges it once, after the loop.
|
|
1182
|
+
out.push({ ...s, invariants: [...s.invariants, ...resultInvariants], body: replaceReturnsWithBreak(s.body, retVar) });
|
|
1183
|
+
out.push({ kind: "return", value: { kind: "var", name: retVar } });
|
|
1184
|
+
if (next && next.kind === "return")
|
|
1185
|
+
i++; // consume the seeded fallthrough return
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
out.push(s);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
1193
|
+
/** True if `name` is referenced as a variable anywhere in `stmts`. Conservative:
|
|
1194
|
+
* counts every occurrence and ignores shadowing — a spurious hit only costs an
|
|
1195
|
+
* unused `let`, whereas a miss would leave a binder unbound. */
|
|
1196
|
+
function stmtsUseVar(stmts, name) {
|
|
1197
|
+
return anyExprInStmts(stmts, e => e.kind === "var" && e.name === name);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Velvet (Lean backend) cannot synthesize a `WPGen` for a monadic statement
|
|
1201
|
+
* `match` on a user inductive in a method body (loom's matcher-WPGen drops a
|
|
1202
|
+
* `sorry`). loom *can* handle method-body `if`s (cf. examples/arrayEquals), so
|
|
1203
|
+
* lower such matches to discriminator `if`-chains: `match x with | .C f.. => B`
|
|
1204
|
+
* becomes `if x.C? then (let f := x.f-destructor; B) else …`. Constructor field
|
|
1205
|
+
* binders become `let`s bound to the (already-supported) union destructor. Option
|
|
1206
|
+
* matches and other non-user matches are left alone (loom handles those). Dafny
|
|
1207
|
+
* is untouched — it keeps the native match.
|
|
1208
|
+
*/
|
|
1209
|
+
function matchToIfChains(stmts) {
|
|
1210
|
+
return stmts.flatMap((s) => {
|
|
1211
|
+
if (s.kind === "if")
|
|
1212
|
+
return [{ ...s, then: matchToIfChains(s.then), else: matchToIfChains(s.else) }];
|
|
1213
|
+
if (s.kind === "while")
|
|
1214
|
+
return [{ ...s, body: matchToIfChains(s.body) }];
|
|
1215
|
+
if (s.kind === "forin")
|
|
1216
|
+
return [{ ...s, body: matchToIfChains(s.body) }];
|
|
1217
|
+
if (s.kind !== "match")
|
|
1218
|
+
return [s];
|
|
1219
|
+
const arms = s.arms.map(a => ({ ...a, body: matchToIfChains(a.body) }));
|
|
1220
|
+
const ctorArms = arms.filter(a => a.pattern.trim() !== "_");
|
|
1221
|
+
const firstCtor = ctorArms[0]?.pattern.trim().split(/\s+/)[0].replace(/^\./, "");
|
|
1222
|
+
const decl = firstCtor
|
|
1223
|
+
? _typeDecls.find(d => (d.kind === "discriminated-union" || d.kind === "string-union") &&
|
|
1224
|
+
((d.variants?.some(v => v.name === firstCtor)) || (d.values?.includes(firstCtor))))
|
|
1225
|
+
: undefined;
|
|
1226
|
+
if (!decl)
|
|
1227
|
+
return [{ ...s, arms }]; // not a user union (e.g. Option) — leave as match
|
|
1228
|
+
const scrutExpr = typeof s.scrutinee === "string" ? { kind: "var", name: s.scrutinee } : s.scrutinee;
|
|
1229
|
+
const defaultArm = arms.find(a => a.pattern.trim() === "_");
|
|
1230
|
+
let elseBranch = defaultArm ? defaultArm.body : [];
|
|
1231
|
+
for (let k = ctorArms.length - 1; k >= 0; k--) {
|
|
1232
|
+
const armBody = ctorArms[k].body;
|
|
1233
|
+
if (armBody.length === 0)
|
|
1234
|
+
continue; // empty arm (no-op) — let it fall through to `else`
|
|
1235
|
+
const toks = ctorArms[k].pattern.trim().split(/\s+/);
|
|
1236
|
+
const ctor = toks[0].replace(/^\./, "");
|
|
1237
|
+
const binders = toks.slice(1);
|
|
1238
|
+
const variant = decl.variants?.find(v => v.name === ctor);
|
|
1239
|
+
// Discriminator condition. A nullary discriminated-union constructor would
|
|
1240
|
+
// need `DecidableEq` for `x = .Ctor` (which such unions don't derive), so
|
|
1241
|
+
// use a match-bool instead. Multi-field ctors already lower to a match-bool;
|
|
1242
|
+
// string-unions derive DecidableEq, so `=` is fine there.
|
|
1243
|
+
const cond = decl.kind === "discriminated-union" && binders.length === 0
|
|
1244
|
+
? { kind: "match", scrutinee: scrutExpr, arms: [
|
|
1245
|
+
{ pattern: `.${ctor}`, body: { kind: "bool", value: true } },
|
|
1246
|
+
{ pattern: "_", body: { kind: "bool", value: false } }
|
|
1247
|
+
] }
|
|
1248
|
+
: { kind: "binop", op: "=", left: scrutExpr, right: { kind: "constructor", name: ctor, type: decl.name } };
|
|
1249
|
+
// Bind only the constructor-field binders the body actually uses, pinning the
|
|
1250
|
+
// owning ctor so the destructor doesn't guess (variants share field names).
|
|
1251
|
+
const lets = [];
|
|
1252
|
+
binders.forEach((b, i) => {
|
|
1253
|
+
const f = variant?.fields[i];
|
|
1254
|
+
if (b !== "_" && f && stmtsUseVar(armBody, b))
|
|
1255
|
+
lets.push({
|
|
1256
|
+
kind: "let", name: b, type: f.type ?? { kind: "unknown" }, mutable: false,
|
|
1257
|
+
value: { kind: "field", obj: scrutExpr, field: f.name, fromUnion: decl.name, ctor },
|
|
1258
|
+
});
|
|
1259
|
+
});
|
|
1260
|
+
elseBranch = [{ kind: "if", cond, then: [...lets, ...armBody], else: elseBranch }];
|
|
1261
|
+
}
|
|
1262
|
+
return elseBranch;
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
/** True if `stmts` contain a `break` not nested inside another while. */
|
|
1266
|
+
function stmtsContainBreak(stmts) {
|
|
1267
|
+
for (const s of stmts) {
|
|
1268
|
+
if (s.kind === "break")
|
|
1269
|
+
return true;
|
|
1270
|
+
if (s.kind === "if" && (stmtsContainBreak(s.then) || stmtsContainBreak(s.else)))
|
|
1271
|
+
return true;
|
|
1272
|
+
if (s.kind === "match" && s.arms.some(a => stmtsContainBreak(a.body)))
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* loom's default loop-exit fact is `¬guard`, which does not hold for a loop that
|
|
1279
|
+
* `break`s (a break exits with the guard still true). Such loops need an explicit
|
|
1280
|
+
* `//@ done_with` in the TS source (`//@ done_with true` when the loop invariant
|
|
1281
|
+
* alone carries the exit facts). Rather than silently supplying one, reject —
|
|
1282
|
+
* the exit fact is part of the spec and belongs beside the invariants.
|
|
1283
|
+
* Lean-only; Dafny derives loop-exit facts from the break sites themselves.
|
|
1284
|
+
*/
|
|
1285
|
+
function requireDoneWithForBreaks(stmts, fnName) {
|
|
1286
|
+
for (const s of stmts) {
|
|
1287
|
+
if (s.kind === "if") {
|
|
1288
|
+
requireDoneWithForBreaks(s.then, fnName);
|
|
1289
|
+
requireDoneWithForBreaks(s.else, fnName);
|
|
1290
|
+
}
|
|
1291
|
+
if (s.kind === "match")
|
|
1292
|
+
for (const a of s.arms)
|
|
1293
|
+
requireDoneWithForBreaks(a.body, fnName);
|
|
1294
|
+
if (s.kind === "while") {
|
|
1295
|
+
if (!s.doneWith && stmtsContainBreak(s.body)) {
|
|
1296
|
+
throw new Error(`${fnName}: loop with break needs //@ done_with on the Lean backend ` +
|
|
1297
|
+
`(an early return in a loop also lowers to a break). ` +
|
|
1298
|
+
`Use //@ done_with true when the loop invariants carry the exit facts.`);
|
|
1299
|
+
}
|
|
1300
|
+
requireDoneWithForBreaks(s.body, fnName);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1109
1304
|
function eliminateTopLevelContinue(stmts) {
|
|
1110
1305
|
const out = [];
|
|
1111
1306
|
for (let i = 0; i < stmts.length; i++) {
|
|
@@ -1359,7 +1554,7 @@ function transformStmt(s, typeDecls) {
|
|
|
1359
1554
|
const recv = s.expr.fn.obj;
|
|
1360
1555
|
const f = s.expr.fn.field;
|
|
1361
1556
|
const isMutating = ((recv.ty.kind === "map" || recv.ty.kind === "set") && (f === "set" || f === "add" || f === "delete")) ||
|
|
1362
|
-
(recv.ty.kind === "array" && f === "push");
|
|
1557
|
+
(recv.ty.kind === "array" && (f === "push" || f === "unshift" || f === "sort"));
|
|
1363
1558
|
if (isMutating && recv.kind === "var") {
|
|
1364
1559
|
const { binds, expr } = liftMethodCalls(s.expr);
|
|
1365
1560
|
return [...binds, { kind: "assign", target: recv.name, value: expr }];
|
|
@@ -1836,6 +2031,7 @@ function transformTypeDecl(d) {
|
|
|
1836
2031
|
else {
|
|
1837
2032
|
return {
|
|
1838
2033
|
kind: "structure", name: d.name,
|
|
2034
|
+
typeParams: d.typeParams,
|
|
1839
2035
|
fields: d.fields.map(f => ({ name: f.name, type: f.type })),
|
|
1840
2036
|
deriving: ["Repr", "Inhabited", "DecidableEq"],
|
|
1841
2037
|
};
|
|
@@ -1854,7 +2050,7 @@ function findReassignedNames(stmts, names) {
|
|
|
1854
2050
|
// Mutating collection calls: s.add(x), m.set(k,v), s.delete(x), arr.push(x)
|
|
1855
2051
|
if (s.kind === "expr" && s.expr.kind === "call" && s.expr.fn.kind === "field" &&
|
|
1856
2052
|
s.expr.fn.obj.kind === "var" && names.has(s.expr.fn.obj.name) &&
|
|
1857
|
-
["add", "set", "delete", "push"].includes(s.expr.fn.field)) {
|
|
2053
|
+
["add", "set", "delete", "push", "unshift"].includes(s.expr.fn.field)) {
|
|
1858
2054
|
found.add(s.expr.fn.obj.name);
|
|
1859
2055
|
}
|
|
1860
2056
|
if (s.kind === "if") {
|
|
@@ -1913,11 +2109,11 @@ function replaceVar(e, name, replacement, narrowing) {
|
|
|
1913
2109
|
}
|
|
1914
2110
|
// ── Top-level transform ──────────────────────────────────────
|
|
1915
2111
|
/** Transform for Lean backend — same logic, Lean options. */
|
|
1916
|
-
export function transformModuleLean(mod, specImport) {
|
|
2112
|
+
export function transformModuleLean(mod, specImport, moduleBase) {
|
|
1917
2113
|
const prev = _opts;
|
|
1918
2114
|
_opts = LEAN_OPTIONS;
|
|
1919
2115
|
try {
|
|
1920
|
-
return transformModule(mod, specImport);
|
|
2116
|
+
return transformModule(mod, specImport, moduleBase);
|
|
1921
2117
|
}
|
|
1922
2118
|
finally {
|
|
1923
2119
|
_opts = prev;
|
|
@@ -1934,7 +2130,7 @@ export function transformModuleDafny(mod) {
|
|
|
1934
2130
|
_opts = prev;
|
|
1935
2131
|
}
|
|
1936
2132
|
}
|
|
1937
|
-
export function transformModule(mod, specImport) {
|
|
2133
|
+
export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
1938
2134
|
_forofCounters.clear();
|
|
1939
2135
|
_liftCounter = 0;
|
|
1940
2136
|
_typeDecls = mod.typeDecls;
|
|
@@ -1987,6 +2183,9 @@ export function transformModule(mod, specImport) {
|
|
|
1987
2183
|
}
|
|
1988
2184
|
}
|
|
1989
2185
|
const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
|
|
2186
|
+
// Lean module base — overridable via `//@ lean-module` (see lsc.ts). Only the
|
|
2187
|
+
// def→types import below reads it; Dafny never passes an override.
|
|
2188
|
+
const moduleBase = moduleBaseOverride ?? base;
|
|
1990
2189
|
// Externs: emit as top-of-file `function {:axiom}` (Dafny) declarations.
|
|
1991
2190
|
// Any `requires`/`ensures` from the source declaration come along so callers
|
|
1992
2191
|
// see the same spec the source itself verified. Substitute `\result` with the
|
|
@@ -2010,12 +2209,18 @@ export function transformModule(mod, specImport) {
|
|
|
2010
2209
|
? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
|
|
2011
2210
|
: [];
|
|
2012
2211
|
if (typeDecls.length > 0 || pureDefs.length > 0 || externDecls.length > 0) {
|
|
2212
|
+
// Declaration order differs by backend. Dafny allows forward references, so
|
|
2213
|
+
// externs go first to be in scope everywhere. Lean requires definition-before-use:
|
|
2214
|
+
// an extern's signature may reference a declared type (e.g. `estimateTokens(m: AgentMessage)`),
|
|
2215
|
+
// so types must precede externs, which in turn precede the pure mirrors that may call them.
|
|
2216
|
+
const decls = _opts.backend === "lean"
|
|
2217
|
+
? [...typeDecls, ...externDecls, ...pureNamespace]
|
|
2218
|
+
: [...externDecls, ...typeDecls, ...pureNamespace];
|
|
2013
2219
|
typesFile = {
|
|
2014
2220
|
comment: " Generated by lsc — Lean types and pure function mirrors.",
|
|
2015
2221
|
imports: typesImports,
|
|
2016
2222
|
options: [],
|
|
2017
|
-
|
|
2018
|
-
decls: [...externDecls, ...typeDecls, ...pureNamespace],
|
|
2223
|
+
decls,
|
|
2019
2224
|
};
|
|
2020
2225
|
}
|
|
2021
2226
|
// Def file: Velvet methods
|
|
@@ -2035,6 +2240,19 @@ export function transformModule(mod, specImport) {
|
|
|
2035
2240
|
let body = pureDefNames.has(fn.name)
|
|
2036
2241
|
? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
|
|
2037
2242
|
: promoteAssignedLets(transformStmts(fn.body, mod.typeDecls));
|
|
2243
|
+
// Lean-only method-body rewrites (Velvet can't WP-synthesize monadic matches
|
|
2244
|
+
// and forbids `return` in loops):
|
|
2245
|
+
// 1. monadic statement-`match` on a user union → discriminator `if`-chains
|
|
2246
|
+
// 2. `return` inside a loop → result var + break (postcondition as invariant)
|
|
2247
|
+
// Dafny keeps the native forms. Breaking loops (including those synthesized
|
|
2248
|
+
// by rewrite 2) must carry a `//@ done_with` in the TS source — enforced here,
|
|
2249
|
+
// since loom's default loop-exit fact `¬guard` does not hold across a break.
|
|
2250
|
+
if (_opts.backend === "lean" && !pureDefNames.has(fn.name)) {
|
|
2251
|
+
body = matchToIfChains(body);
|
|
2252
|
+
const resultInvariants = fn.ensures.map(e => replaceVar(transformExpr(e), "\\result", { kind: "var", name: "_loopRet" }));
|
|
2253
|
+
body = eliminateReturnInLoops(body, fn.returnTy, resultInvariants);
|
|
2254
|
+
requireDoneWithForBreaks(body, fn.name);
|
|
2255
|
+
}
|
|
2038
2256
|
// Shadow reassigned parameters with mutable locals
|
|
2039
2257
|
const paramNames = new Set(fn.params.map(p => p.name));
|
|
2040
2258
|
const reassigned = findReassignedNames(fn.body, paramNames);
|
|
@@ -2081,7 +2299,7 @@ export function transformModule(mod, specImport) {
|
|
|
2081
2299
|
methods: classMethods,
|
|
2082
2300
|
};
|
|
2083
2301
|
});
|
|
2084
|
-
const defImport = specImport ?? (typesFile ? `«${
|
|
2302
|
+
const defImport = specImport ?? (typesFile ? `«${moduleBase}.types»` : null);
|
|
2085
2303
|
const defBaseImports = defImport ? [defImport] : ["LemmaScript"];
|
|
2086
2304
|
const defFile = {
|
|
2087
2305
|
comment: " Generated by lsc from " + (mod.file.split("/").pop() ?? "") + "\n Do not edit — re-run `lsc gen` to regenerate.",
|
package/tools/dist/types.js
CHANGED
|
@@ -55,6 +55,20 @@ function tyFromTypeNode(tn) {
|
|
|
55
55
|
if (normalized.length === 1 && "syntheticBool" in normalized[0])
|
|
56
56
|
return { kind: "bool" };
|
|
57
57
|
const nonNullish = normalized.filter(a => "syntheticBool" in a || !isNullish(a.node));
|
|
58
|
+
// Inline string-literal union (`"a" | "b"`, not a //@ declare-type): no datatype
|
|
59
|
+
// to resolve against, so lower to plain string (the arms are strings; == holds).
|
|
60
|
+
const isStrLit = (a) => !("syntheticBool" in a) && Node.isLiteralTypeNode(a.node) && a.node.getLiteral().getKind() === SyntaxKind.StringLiteral;
|
|
61
|
+
if (nonNullish.length >= 2 && nonNullish.every(isStrLit)) {
|
|
62
|
+
// Keep the literal members so `rec[k]` can lower to an equality chain.
|
|
63
|
+
const values = nonNullish.map(a => {
|
|
64
|
+
const lit = a.node;
|
|
65
|
+
const inner = Node.isLiteralTypeNode(lit) ? lit.getLiteral() : lit;
|
|
66
|
+
return Node.isStringLiteral(inner) ? inner.getLiteralValue() : inner.getText();
|
|
67
|
+
});
|
|
68
|
+
return normalized.some(a => !("syntheticBool" in a) && isNullish(a.node))
|
|
69
|
+
? { kind: "optional", inner: { kind: "string", values } }
|
|
70
|
+
: { kind: "string", values };
|
|
71
|
+
}
|
|
58
72
|
if (nonNullish.length === 1 && normalized.length >= 2) {
|
|
59
73
|
const sole = nonNullish[0];
|
|
60
74
|
const inner = "syntheticBool" in sole ? { kind: "bool" } : tyFromTypeNode(sole.node);
|