lemmascript 0.5.5 → 0.5.7
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 +2 -1
- package/tools/dist/dafny-commands.js +3 -1
- package/tools/dist/dafny-emit.js +47 -5
- package/tools/dist/extract.js +73 -31
- package/tools/dist/guard-command.js +238 -0
- package/tools/dist/lean-emit.js +29 -6
- package/tools/dist/narrow.js +30 -6
- package/tools/dist/resolve.js +13 -1
- package/tools/dist/specparser.js +37 -15
- package/tools/dist/transform.js +193 -47
- package/tools/dist/emit.js +0 -253
package/tools/dist/lean-emit.js
CHANGED
|
@@ -77,6 +77,7 @@ function escapeName(name) {
|
|
|
77
77
|
}
|
|
78
78
|
// ── Operator precedence (for parenthesization) ──────────────
|
|
79
79
|
const PREC = {
|
|
80
|
+
"↔": 0, // Lean: Iff (20) binds looser than → (25)
|
|
80
81
|
"→": 1, "∨": 2, "∧": 3,
|
|
81
82
|
"=": 4, "≠": 4, "≥": 4, "≤": 4, ">": 4, "<": 4,
|
|
82
83
|
"+": 5, "-": 5, "++": 5, "arrayConcat": 5, "*": 6, "/": 6, "%": 6,
|
|
@@ -96,15 +97,15 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
96
97
|
if (method === "some")
|
|
97
98
|
return `${obj}.${monadic ? "anyM" : "any"} ${args[0]}`;
|
|
98
99
|
if (method === "includes")
|
|
99
|
-
return `${obj}.contains ${args[0]}`;
|
|
100
|
+
return args.length > 1 ? `(${obj}.extract ${args[1]} ${obj}.size).contains ${args[0]}` : `${obj}.contains ${args[0]}`;
|
|
100
101
|
if (method === "find")
|
|
101
102
|
return `${obj}.find? ${args[0]}`;
|
|
102
103
|
if (method === "with")
|
|
103
104
|
return `${obj}.set! ${args[0]} ${args[1]}`;
|
|
104
105
|
if (method === "push")
|
|
105
|
-
return `Array.push ${obj} ${args[0]}`;
|
|
106
|
+
return args.length === 1 ? `Array.push ${obj} ${args[0]}` : `${obj} ++ #[${args.join(", ")}]`;
|
|
106
107
|
if (method === "concat")
|
|
107
|
-
return `Array.push ${obj} ${args[0]}`;
|
|
108
|
+
return args.length === 1 ? `Array.push ${obj} ${args[0]}` : `${obj} ++ #[${args.join(", ")}]`;
|
|
108
109
|
// arr.slice → Array.extract. No-arg slice is a full copy (Array is a value
|
|
109
110
|
// type in Lean, so the receiver itself); one arg drops the prefix, two args
|
|
110
111
|
// give the half-open range. Matches JS for non-negative bounds (negative
|
|
@@ -184,7 +185,7 @@ function emitExpr(e, parentPrec) {
|
|
|
184
185
|
case "emptySet": return `Std.HashSet.empty`;
|
|
185
186
|
case "methodCall": {
|
|
186
187
|
const obj = emitExpr(e.obj);
|
|
187
|
-
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "methodCall";
|
|
188
|
+
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "methodCall" || e.obj.kind === "if" || e.obj.kind === "let";
|
|
188
189
|
const receiver = wrap ? `(${obj})` : obj;
|
|
189
190
|
const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app" || a.kind === "methodCall") ? `(${emitExpr(a)})` : emitExpr(a));
|
|
190
191
|
return emitMethodCall(e.objTy.kind, e.method, e.monadic, receiver, args);
|
|
@@ -213,11 +214,23 @@ function emitExpr(e, parentPrec) {
|
|
|
213
214
|
return `${wrap ? `(${recv})` : recv}.contains ${emitExpr(e.left)}`;
|
|
214
215
|
}
|
|
215
216
|
const op = e.op === "arrayConcat" ? "++" : e.op;
|
|
216
|
-
|
|
217
|
+
// ↔ does not chain in Lean — a nested iff operand needs parens.
|
|
218
|
+
const childPrec = e.op === "↔" ? prec(e.op) + 1 : prec(e.op);
|
|
219
|
+
// `-`, `/`, `%` are left-associative and non-associative, so an equal-
|
|
220
|
+
// precedence right operand must be parenthesized: `a - (b - c)` would
|
|
221
|
+
// otherwise emit as `a - b - c`, i.e. `(a - b) - c`.
|
|
222
|
+
const rightPrec = e.op === "↔" ? childPrec
|
|
223
|
+
: ["-", "/", "%"].includes(e.op) ? prec(e.op) + 1 : childPrec;
|
|
224
|
+
const s = `${wrapOperand(e.left, childPrec)} ${op} ${wrapOperand(e.right, rightPrec)}`;
|
|
217
225
|
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
218
226
|
}
|
|
219
227
|
case "implies": {
|
|
220
|
-
|
|
228
|
+
// Premises bind at →'s level: a nested-implication premise must keep its
|
|
229
|
+
// parens (→ is right-associative, so `(a → b) → c` ≠ `a → b → c`), and
|
|
230
|
+
// ↔ binds looser than → in Lean. The conclusion is the right-assoc tail,
|
|
231
|
+
// where a nested implication is safe bare — only ↔ needs parens there.
|
|
232
|
+
const wrapIff = (x) => x.kind === "binop" && x.op === "↔" ? `(${emitExpr(x)})` : undefined;
|
|
233
|
+
const parts = [...e.premises.map(p => wrapOperand(p, prec("→"))), wrapIff(e.conclusion) ?? emitExpr(e.conclusion)];
|
|
221
234
|
const s = parts.join(" → ");
|
|
222
235
|
return parentPrec !== undefined ? `(${s})` : s;
|
|
223
236
|
}
|
|
@@ -231,6 +244,16 @@ function emitExpr(e, parentPrec) {
|
|
|
231
244
|
// SetToSeq → .toArray for Lean (HashSet has native toArray)
|
|
232
245
|
if (e.fn === "SetToSeq" && args.length === 1)
|
|
233
246
|
return `${args[0]}.toArray`;
|
|
247
|
+
if (e.fn === "SetFromSeq" && args.length === 1)
|
|
248
|
+
return `Std.HashSet.ofList ${args[0]}.toList`;
|
|
249
|
+
if (e.fn === "ToString" && args.length === 1)
|
|
250
|
+
return `toString ${args[0]}`;
|
|
251
|
+
// JSRem (JS truncated remainder) → Lean's native truncated `Int.tmod`
|
|
252
|
+
if (e.fn === "JSRem" && args.length === 2)
|
|
253
|
+
return `Int.tmod ${args[0]} ${args[1]}`;
|
|
254
|
+
// JSTruncDiv (JS truncated bigint division) → Lean's native `Int.tdiv`
|
|
255
|
+
if (e.fn === "JSTruncDiv" && args.length === 2)
|
|
256
|
+
return `Int.tdiv ${args[0]} ${args[1]}`;
|
|
234
257
|
// perm(a, b) → `List.Perm` on the underlying lists. Dafny lowers it to
|
|
235
258
|
// `multiset(a) == multiset(b)`; the Lean image is `a.toList ~ b.toList`,
|
|
236
259
|
// which mathlib's `List.Perm` provides (reflexivity, symmetry,
|
package/tools/dist/narrow.js
CHANGED
|
@@ -249,6 +249,8 @@ function ruleEarlyReturnConsume(s, rest) {
|
|
|
249
249
|
const noneBranch = check.negated ? s.then : s.else;
|
|
250
250
|
if (someBranch.length !== 0)
|
|
251
251
|
return null;
|
|
252
|
+
if (!isTerminating(noneBranch))
|
|
253
|
+
return null;
|
|
252
254
|
return {
|
|
253
255
|
kind: "someMatch",
|
|
254
256
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
@@ -292,11 +294,14 @@ function ruleEarlyReturnOrChain(s, rest) {
|
|
|
292
294
|
let inner = rest;
|
|
293
295
|
for (let i = checks.length - 1; i >= 0; i--) {
|
|
294
296
|
const check = checks[i];
|
|
297
|
+
const someBody = canBeFalsy(check)
|
|
298
|
+
? [{ kind: "if", cond: bound(check), then: inner, else: s.then }]
|
|
299
|
+
: inner;
|
|
295
300
|
inner = [{
|
|
296
301
|
kind: "someMatch",
|
|
297
302
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
298
303
|
binder: check.binderHint,
|
|
299
|
-
someBody
|
|
304
|
+
someBody,
|
|
300
305
|
noneBody: s.then,
|
|
301
306
|
}];
|
|
302
307
|
}
|
|
@@ -582,11 +587,14 @@ function ruleIfAndOptional(s) {
|
|
|
582
587
|
return null;
|
|
583
588
|
const { check, restCond } = extracted;
|
|
584
589
|
const innerIf = { kind: "if", cond: restCond, then: s.then, else: [] };
|
|
590
|
+
const someBody = canBeFalsy(check)
|
|
591
|
+
? [{ kind: "if", cond: bound(check), then: [walkStmt(innerIf)], else: [] }]
|
|
592
|
+
: [walkStmt(innerIf)];
|
|
585
593
|
return {
|
|
586
594
|
kind: "someMatch",
|
|
587
595
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
588
596
|
binder: check.binderHint,
|
|
589
|
-
someBody
|
|
597
|
+
someBody,
|
|
590
598
|
noneBody: [],
|
|
591
599
|
};
|
|
592
600
|
}
|
|
@@ -776,8 +784,16 @@ function ruleDiscriminantChain(stmts) {
|
|
|
776
784
|
}
|
|
777
785
|
if (cases.length === 0)
|
|
778
786
|
return null;
|
|
787
|
+
// If every case terminates, the trailing statements are the default arm
|
|
788
|
+
// (preserving the clean dispatch-as-expression shape). Otherwise the tail runs
|
|
789
|
+
// after the match for every variant, so leave it to the caller (empty default)
|
|
790
|
+
// rather than mis-routing it into the default arm only.
|
|
791
|
+
if (cases.every(c => isTerminating(c.body))) {
|
|
792
|
+
return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
|
|
793
|
+
cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
|
|
794
|
+
}
|
|
779
795
|
return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
|
|
780
|
-
cases, fallthrough:
|
|
796
|
+
cases, fallthrough: [] }, consumed };
|
|
781
797
|
}
|
|
782
798
|
/** Rule (list-level): `if (x.kind !== "v") terminate; rest` → tagMatch
|
|
783
799
|
* with cases = [{ variant: v, body: rest }] and fallthrough = terminate. */
|
|
@@ -809,12 +825,16 @@ function ruleLetCondAndOptional(s) {
|
|
|
809
825
|
if (!extracted)
|
|
810
826
|
return null;
|
|
811
827
|
const { check, restCond } = extracted;
|
|
828
|
+
const assignIf = { kind: "if", cond: restCond,
|
|
829
|
+
then: [{ kind: "assign", target: s.name, value: s.init.then }], else: [] };
|
|
830
|
+
const someBody = canBeFalsy(check)
|
|
831
|
+
? [{ kind: "if", cond: bound(check), then: [assignIf], else: [] }]
|
|
832
|
+
: [assignIf];
|
|
812
833
|
const sm = {
|
|
813
834
|
kind: "someMatch",
|
|
814
835
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
815
836
|
binder: check.binderHint,
|
|
816
|
-
someBody
|
|
817
|
-
then: [{ kind: "assign", target: s.name, value: s.init.then }], else: [] }],
|
|
837
|
+
someBody,
|
|
818
838
|
noneBody: [],
|
|
819
839
|
};
|
|
820
840
|
return [
|
|
@@ -889,11 +909,15 @@ function ruleConditionalAndOptional(e) {
|
|
|
889
909
|
kind: "conditional",
|
|
890
910
|
cond: restCond, then: e.then, else: e.else, ty: e.ty,
|
|
891
911
|
};
|
|
912
|
+
const someExpr = walkExpr(innerCond);
|
|
913
|
+
const someBody = canBeFalsy(check)
|
|
914
|
+
? { kind: "conditional", cond: bound(check), then: someExpr, else: e.else, ty: e.ty }
|
|
915
|
+
: someExpr;
|
|
892
916
|
return {
|
|
893
917
|
kind: "someMatch",
|
|
894
918
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
895
919
|
binder: check.binderHint,
|
|
896
|
-
someBody
|
|
920
|
+
someBody, noneBody: e.else, ty: e.ty,
|
|
897
921
|
};
|
|
898
922
|
}
|
|
899
923
|
/** Rule (statement): `if (<rest> && Array.isArray(path) && <more>) then [else]`
|
package/tools/dist/resolve.js
CHANGED
|
@@ -685,7 +685,9 @@ function resolveExpr(e, ctx) {
|
|
|
685
685
|
}
|
|
686
686
|
}
|
|
687
687
|
let ty = { kind: "unknown" };
|
|
688
|
-
|
|
688
|
+
// <==> is bool like the comparisons; unlike ==>, neither side narrows
|
|
689
|
+
// the other (no premise to assume).
|
|
690
|
+
if (["===", "!==", ">=", "<=", ">", "<", "in", "<==>"].includes(e.op))
|
|
689
691
|
ty = { kind: "bool" };
|
|
690
692
|
else if (e.op === "&&")
|
|
691
693
|
ty = right.ty;
|
|
@@ -727,6 +729,16 @@ function resolveExpr(e, ctx) {
|
|
|
727
729
|
const fn = { kind: "var", name: "Perm", ty: { kind: "unknown" } };
|
|
728
730
|
return { kind: "call", fn, args: [a, b], ty: { kind: "bool" }, callKind: "pure" };
|
|
729
731
|
}
|
|
732
|
+
// new Set(arr): build a deduplicated set from the array's elements (extract
|
|
733
|
+
// marks the array form `__setFromArray`). Lowers to the SetFromSeq preamble
|
|
734
|
+
// (Dafny `set x | x in s`); size/membership are then set semantics.
|
|
735
|
+
if (e.fn.kind === "var" && e.fn.name === "__setFromArray" && e.args.length === 1) {
|
|
736
|
+
const arr = resolveExpr(e.args[0], ctx);
|
|
737
|
+
if (arr.ty.kind !== "array")
|
|
738
|
+
throw new Error(`new Set(...) expects an array argument (got ${arr.ty.kind})`);
|
|
739
|
+
const fn = { kind: "var", name: "SetFromSeq", ty: { kind: "unknown" } };
|
|
740
|
+
return { kind: "call", fn, args: [arr], ty: { kind: "set", elem: arr.ty.elem }, callKind: "pure" };
|
|
741
|
+
}
|
|
730
742
|
// Extern dispatch: `NS.method(args)` where NS.method is declared via
|
|
731
743
|
// `//@ extern`. Rewrite into a flat-name call (`NS_method(args)`) so the
|
|
732
744
|
// rest of the pipeline sees an ordinary pure function. The extern's
|
package/tools/dist/specparser.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Spec expression parser.
|
|
3
3
|
* Parses //@ annotation expressions into RawExpr AST nodes.
|
|
4
4
|
*/
|
|
5
|
-
const MULTI_OPS = ["==>", "===", "!==", "==", "!=", ">=", "<=", "&&", "||"];
|
|
5
|
+
const MULTI_OPS = ["<==>", "==>", "===", "!==", "==", "!=", ">=", "<=", "&&", "||"];
|
|
6
6
|
function tokenize(input) {
|
|
7
7
|
const tokens = [];
|
|
8
8
|
let i = 0;
|
|
@@ -20,8 +20,22 @@ function tokenize(input) {
|
|
|
20
20
|
const quote = input[i];
|
|
21
21
|
i++;
|
|
22
22
|
let s = "";
|
|
23
|
-
while (i < input.length && input[i] !== quote)
|
|
24
|
-
|
|
23
|
+
while (i < input.length && input[i] !== quote) {
|
|
24
|
+
if (input[i] === "\\") {
|
|
25
|
+
// Standard escapes, where TS source, Dafny, and Lean all agree.
|
|
26
|
+
// The emitters re-escape on output, so the round trip is faithful.
|
|
27
|
+
const esc = input[i + 1];
|
|
28
|
+
const mapped = esc === "n" ? "\n" : esc === "r" ? "\r" : esc === "t" ? "\t"
|
|
29
|
+
: esc === "0" ? "\0" : esc === "\\" || esc === '"' || esc === "'" ? esc : null;
|
|
30
|
+
if (mapped === null)
|
|
31
|
+
throw new Error(`Unsupported string escape '\\${esc}' at ${i} in: ${input}`);
|
|
32
|
+
s += mapped;
|
|
33
|
+
i += 2;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
s += input[i++];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
25
39
|
if (i < input.length)
|
|
26
40
|
i++;
|
|
27
41
|
tokens.push({ type: "str", value: s });
|
|
@@ -103,11 +117,19 @@ class Parser {
|
|
|
103
117
|
return false;
|
|
104
118
|
}
|
|
105
119
|
parse() {
|
|
106
|
-
const r = this.
|
|
120
|
+
const r = this.parseIff();
|
|
107
121
|
if (this.pos < this.tokens.length)
|
|
108
122
|
throw new Error(`Unexpected: ${JSON.stringify(this.peek())}`);
|
|
109
123
|
return r;
|
|
110
124
|
}
|
|
125
|
+
// <==> binds loosest (Dafny precedence: a ==> b <==> c is (a ==> b) <==> c),
|
|
126
|
+
// right-associative like ==> — immaterial semantically, iff is associative.
|
|
127
|
+
parseIff() {
|
|
128
|
+
const left = this.parseImplies();
|
|
129
|
+
if (this.match("op", "<==>"))
|
|
130
|
+
return { kind: "binop", op: "<==>", left, right: this.parseIff() };
|
|
131
|
+
return left;
|
|
132
|
+
}
|
|
111
133
|
parseImplies() {
|
|
112
134
|
const left = this.parseTernary();
|
|
113
135
|
if (this.match("op", "==>"))
|
|
@@ -117,9 +139,9 @@ class Parser {
|
|
|
117
139
|
parseTernary() {
|
|
118
140
|
const cond = this.parseOr();
|
|
119
141
|
if (this.match("op", "?")) {
|
|
120
|
-
const then_ = this.
|
|
142
|
+
const then_ = this.parseIff();
|
|
121
143
|
this.expect("punc", ":");
|
|
122
|
-
const else_ = this.
|
|
144
|
+
const else_ = this.parseIff();
|
|
123
145
|
return { kind: "conditional", cond, then: then_, else: else_ };
|
|
124
146
|
}
|
|
125
147
|
return cond;
|
|
@@ -187,16 +209,16 @@ class Parser {
|
|
|
187
209
|
expr = { kind: "field", obj: expr, field: this.expect("ident").value };
|
|
188
210
|
}
|
|
189
211
|
else if (this.match("punc", "[")) {
|
|
190
|
-
const idx = this.
|
|
212
|
+
const idx = this.parseIff();
|
|
191
213
|
this.expect("punc", "]");
|
|
192
214
|
expr = { kind: "index", obj: expr, idx };
|
|
193
215
|
}
|
|
194
216
|
else if (this.match("punc", "(")) {
|
|
195
217
|
const args = [];
|
|
196
218
|
if (!this.match("punc", ")")) {
|
|
197
|
-
args.push(this.
|
|
219
|
+
args.push(this.parseIff());
|
|
198
220
|
while (this.match("punc", ","))
|
|
199
|
-
args.push(this.
|
|
221
|
+
args.push(this.parseIff());
|
|
200
222
|
this.expect("punc", ")");
|
|
201
223
|
}
|
|
202
224
|
expr = { kind: "call", fn: expr, args };
|
|
@@ -276,7 +298,7 @@ class Parser {
|
|
|
276
298
|
varType = ty;
|
|
277
299
|
}
|
|
278
300
|
this.expect("punc", ",");
|
|
279
|
-
const body = this.
|
|
301
|
+
const body = this.parseIff();
|
|
280
302
|
this.expect("punc", ")");
|
|
281
303
|
return { kind: q, var: v, varType, body };
|
|
282
304
|
}
|
|
@@ -285,7 +307,7 @@ class Parser {
|
|
|
285
307
|
}
|
|
286
308
|
if (t.type === "punc" && t.value === "(") {
|
|
287
309
|
this.advance();
|
|
288
|
-
const expr = this.
|
|
310
|
+
const expr = this.parseIff();
|
|
289
311
|
this.expect("punc", ")");
|
|
290
312
|
return expr;
|
|
291
313
|
}
|
|
@@ -293,9 +315,9 @@ class Parser {
|
|
|
293
315
|
this.advance();
|
|
294
316
|
const elems = [];
|
|
295
317
|
if (!this.match("punc", "]")) {
|
|
296
|
-
elems.push(this.
|
|
318
|
+
elems.push(this.parseIff());
|
|
297
319
|
while (this.match("punc", ","))
|
|
298
|
-
elems.push(this.
|
|
320
|
+
elems.push(this.parseIff());
|
|
299
321
|
this.expect("punc", "]");
|
|
300
322
|
}
|
|
301
323
|
return { kind: "arrayLiteral", elems };
|
|
@@ -306,11 +328,11 @@ class Parser {
|
|
|
306
328
|
if (!this.match("punc", "}")) {
|
|
307
329
|
const name = this.expect("ident").value;
|
|
308
330
|
this.expect("punc", ":");
|
|
309
|
-
fields.push({ name, value: this.
|
|
331
|
+
fields.push({ name, value: this.parseIff() });
|
|
310
332
|
while (this.match("punc", ",")) {
|
|
311
333
|
const n = this.expect("ident").value;
|
|
312
334
|
this.expect("punc", ":");
|
|
313
|
-
fields.push({ name: n, value: this.
|
|
335
|
+
fields.push({ name: n, value: this.parseIff() });
|
|
314
336
|
}
|
|
315
337
|
this.expect("punc", "}");
|
|
316
338
|
}
|