lemmascript 0.4.0 → 0.5.0
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 +17 -12
- package/package.json +4 -1
- package/tools/dist/dafny-emit.js +285 -12
- package/tools/dist/extract.js +1055 -161
- package/tools/dist/info-command.js +38 -0
- package/tools/dist/lean-emit.js +28 -2
- package/tools/dist/lsc.js +16 -6
- package/tools/dist/narrow.js +211 -16
- package/tools/dist/peephole.js +5 -2
- package/tools/dist/resolve.js +375 -46
- package/tools/dist/specparser.js +6 -0
- package/tools/dist/transform.js +400 -42
- package/tools/dist/types.js +128 -69
package/tools/dist/transform.js
CHANGED
|
@@ -24,6 +24,7 @@ function mapExpr(e, f) {
|
|
|
24
24
|
case "emptyMap":
|
|
25
25
|
case "emptySet":
|
|
26
26
|
case "havoc": return e;
|
|
27
|
+
case "mapLiteral": return { ...e, entries: e.entries.map(en => ({ key: r(en.key), value: r(en.value) })) };
|
|
27
28
|
case "constructor": return e.args ? { ...e, args: e.args.map(r) } : e;
|
|
28
29
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
29
30
|
case "unop": return { ...e, expr: r(e.expr) };
|
|
@@ -80,7 +81,6 @@ function mapTExpr(e, f) {
|
|
|
80
81
|
case "num":
|
|
81
82
|
case "str":
|
|
82
83
|
case "bool":
|
|
83
|
-
case "result":
|
|
84
84
|
case "havoc": return e;
|
|
85
85
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
86
86
|
case "unop": return { ...e, expr: r(e.expr) };
|
|
@@ -187,6 +187,40 @@ const BOOL_OP_MAP = {
|
|
|
187
187
|
...OP_MAP, "===": "==", "!==": "!=",
|
|
188
188
|
};
|
|
189
189
|
function transformExpr(e) { return lowerExpr(e, null); }
|
|
190
|
+
/** Reduce an if/let/return-shaped statement body to a single expression, for
|
|
191
|
+
* expression-only lambda bodies. Returns null for shapes that can't be a pure
|
|
192
|
+
* expression (loops, assignments, bare side effects), so callers leave the
|
|
193
|
+
* body as statements. A `return` is terminal — statements after it are
|
|
194
|
+
* unreachable and dropped.
|
|
195
|
+
* [return e] → e
|
|
196
|
+
* [let x = e, …rest] → Expr.let(x, e, flatten(rest))
|
|
197
|
+
* [if (c) thenStmts elseStmts, …rest] → Expr.if(c, …) where each branch
|
|
198
|
+
* absorbs `rest` if it doesn't already terminate with a return. */
|
|
199
|
+
function flattenLambdaBody(stmts) {
|
|
200
|
+
if (stmts.length === 0)
|
|
201
|
+
return null;
|
|
202
|
+
const first = stmts[0];
|
|
203
|
+
const rest = stmts.slice(1);
|
|
204
|
+
if (first.kind === "return")
|
|
205
|
+
return first.value;
|
|
206
|
+
if (first.kind === "let" && !first.mutable) {
|
|
207
|
+
const body = flattenLambdaBody(rest);
|
|
208
|
+
return body === null ? null : { kind: "let", name: first.name, value: first.value, body };
|
|
209
|
+
}
|
|
210
|
+
if (first.kind === "if") {
|
|
211
|
+
const thenTerminates = flattenLambdaBody(first.then);
|
|
212
|
+
if (thenTerminates !== null) {
|
|
213
|
+
// then-branch yields a value (ends in return) → `rest` is the else path.
|
|
214
|
+
const elseExpr = flattenLambdaBody(first.else.length > 0 ? [...first.else, ...rest] : rest);
|
|
215
|
+
return elseExpr === null ? null : { kind: "if", cond: first.cond, then: thenTerminates, else: elseExpr };
|
|
216
|
+
}
|
|
217
|
+
// then-branch falls through → both branches continue into `rest`.
|
|
218
|
+
const thenExpr = flattenLambdaBody([...first.then, ...rest]);
|
|
219
|
+
const elseExpr = flattenLambdaBody(first.else.length > 0 ? [...first.else, ...rest] : rest);
|
|
220
|
+
return thenExpr === null || elseExpr === null ? null : { kind: "if", cond: first.cond, then: thenExpr, else: elseExpr };
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
190
224
|
/**
|
|
191
225
|
* Lower a typed expression to Backend IR.
|
|
192
226
|
*
|
|
@@ -196,6 +230,18 @@ function transformExpr(e) { return lowerExpr(e, null); }
|
|
|
196
230
|
* a method call can appear inline in TS. It does NOT propagate into
|
|
197
231
|
* field, index, record, forall, or exists sub-expressions.
|
|
198
232
|
*/
|
|
233
|
+
/** JS truthiness coercion for `if`/`while`/`?:` conditions.
|
|
234
|
+
* Dafny requires bool; TS treats number/string/array as truthy when non-empty.
|
|
235
|
+
* Optional conds are handled separately by narrow.ts (rewritten to someMatch). */
|
|
236
|
+
function coerceCondToBool(cond, ty) {
|
|
237
|
+
if (ty.kind === "bool")
|
|
238
|
+
return cond;
|
|
239
|
+
if (ty.kind === "int" || ty.kind === "nat")
|
|
240
|
+
return { kind: "binop", op: ">", left: cond, right: { kind: "num", value: 0 } };
|
|
241
|
+
if (ty.kind === "string" || ty.kind === "array")
|
|
242
|
+
return { kind: "binop", op: ">", left: { kind: "field", obj: cond, field: "size" }, right: { kind: "num", value: 0 } };
|
|
243
|
+
return cond;
|
|
244
|
+
}
|
|
199
245
|
/** Wrap an expression in Some/None for optional-typed conditionals.
|
|
200
246
|
* If the raw TExpr is `undefined`, emit `.none`; otherwise wrap in `Some`. */
|
|
201
247
|
function wrapOptionalBranch(expr, raw) {
|
|
@@ -225,7 +271,6 @@ function lowerExpr(e, binds) {
|
|
|
225
271
|
case "var": return { kind: "var", name: e.name };
|
|
226
272
|
case "num": return { kind: "num", value: e.value };
|
|
227
273
|
case "bool": return { kind: "bool", value: e.value };
|
|
228
|
-
case "result": return { kind: "var", name: "res" };
|
|
229
274
|
case "str":
|
|
230
275
|
if (e.ty.kind === "user")
|
|
231
276
|
return { kind: "constructor", name: e.value, type: e.ty.name };
|
|
@@ -336,10 +381,27 @@ function lowerExpr(e, binds) {
|
|
|
336
381
|
(e.left.ty.kind === "user" && e.right.ty.kind === "string"))) {
|
|
337
382
|
const left = lowerExpr(e.left, binds);
|
|
338
383
|
const right = lowerExpr(e.right, binds);
|
|
384
|
+
// `s || undefined` produces `Option<string>` — wrap the truthy branch in Some.
|
|
385
|
+
const rightIsUndef = e.right.kind === "var" && e.right.name === "undefined";
|
|
339
386
|
return {
|
|
340
387
|
kind: "if",
|
|
341
388
|
cond: { kind: "binop", op: ">", left: { kind: "field", obj: left, field: "size" }, right: { kind: "num", value: 0 } },
|
|
342
|
-
then:
|
|
389
|
+
then: rightIsUndef ? { kind: "app", fn: "Some", args: [left] } : left,
|
|
390
|
+
else: right,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// `bool || undefined` → `if bool then Some(bool) else None`. Used in
|
|
394
|
+
// optional-field initialization where the source assigns a truthy/false
|
|
395
|
+
// bool to a `T?` field. Without this, emit produces `bool || None`,
|
|
396
|
+
// which Dafny rejects (bool || Option<?> is ill-typed).
|
|
397
|
+
if (e.op === "||" && e.left.ty.kind === "bool" &&
|
|
398
|
+
e.right.kind === "var" && e.right.name === "undefined") {
|
|
399
|
+
const left = lowerExpr(e.left, binds);
|
|
400
|
+
return {
|
|
401
|
+
kind: "if",
|
|
402
|
+
cond: left,
|
|
403
|
+
then: { kind: "app", fn: "Some", args: [left] },
|
|
404
|
+
else: { kind: "var", name: "undefined" },
|
|
343
405
|
};
|
|
344
406
|
}
|
|
345
407
|
// int + string → NatToString(int) + string (string concatenation)
|
|
@@ -397,6 +459,25 @@ function lowerExpr(e, binds) {
|
|
|
397
459
|
return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
|
|
398
460
|
}
|
|
399
461
|
case "call": {
|
|
462
|
+
// Array.isArray(x) on a synth array-union (discriminant "__isArray__")
|
|
463
|
+
// → constructor predicate `x.ArrayBranch?`. Used in spec ensures and
|
|
464
|
+
// anywhere `Array.isArray` escapes the narrowing rule (narrow rewrites
|
|
465
|
+
// top-level if-cond Array.isArray uses; this catches the rest).
|
|
466
|
+
if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Array" &&
|
|
467
|
+
e.fn.field === "isArray" && e.args.length === 1) {
|
|
468
|
+
const arg = e.args[0];
|
|
469
|
+
if (arg.ty.kind === "user") {
|
|
470
|
+
const baseName = arg.ty.name.includes("<") ? arg.ty.name.slice(0, arg.ty.name.indexOf("<")) : arg.ty.name;
|
|
471
|
+
const decl = _typeDecls.find(d => d.name === baseName);
|
|
472
|
+
if (decl?.kind === "discriminated-union" && decl.discriminant === "__isArray__") {
|
|
473
|
+
return {
|
|
474
|
+
kind: "binop", op: "=",
|
|
475
|
+
left: lowerExpr(arg, binds),
|
|
476
|
+
right: { kind: "constructor", name: "ArrayBranch", type: arg.ty.name },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
400
481
|
// Math.abs/min/max → preamble functions
|
|
401
482
|
if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math") {
|
|
402
483
|
if (e.fn.field === "abs" && e.args.length === 1)
|
|
@@ -536,6 +617,19 @@ function lowerExpr(e, binds) {
|
|
|
536
617
|
if (e.fields.length === 0 && !e.spread && e.ty.kind === "map") {
|
|
537
618
|
return { kind: "emptyMap" };
|
|
538
619
|
}
|
|
620
|
+
// Non-empty record literal with map type — emit as a flat Dafny map
|
|
621
|
+
// literal `map[k1 := v1, k2 := v2, ...]`. (A chain of `m["k" := v]`
|
|
622
|
+
// works for a handful of entries but Dafny's type resolver stack-
|
|
623
|
+
// overflows on hundreds; the flat form is fine at any size.)
|
|
624
|
+
if (e.fields.length > 0 && !e.spread && e.ty.kind === "map") {
|
|
625
|
+
return {
|
|
626
|
+
kind: "mapLiteral",
|
|
627
|
+
entries: e.fields.map(f => ({
|
|
628
|
+
key: { kind: "str", value: f.name },
|
|
629
|
+
value: lowerExpr(f.value, binds),
|
|
630
|
+
})),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
539
633
|
return { kind: "record", spread: null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
|
|
540
634
|
}
|
|
541
635
|
case "arrayLiteral":
|
|
@@ -547,14 +641,29 @@ function lowerExpr(e, binds) {
|
|
|
547
641
|
if (e.ty.kind === "set")
|
|
548
642
|
return { kind: "app", fn: "SetLiteral", args: e.elems.map(el => lowerExpr(el, binds)) };
|
|
549
643
|
return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
|
|
550
|
-
case "lambda":
|
|
551
|
-
|
|
644
|
+
case "lambda": {
|
|
645
|
+
const body = transformStmts(e.body, []);
|
|
646
|
+
// Flatten an if/let/return-shaped multi-statement body into a single
|
|
647
|
+
// `return <expr>` so both backends' single-return-lambda fast path emits
|
|
648
|
+
// it (Dafny lambdas are expression-only; Lean prefers the expression form
|
|
649
|
+
// over a `do` block). Bodies with shapes we can't reduce (loops, bare
|
|
650
|
+
// side effects) are left as-is.
|
|
651
|
+
const flat = flattenLambdaBody(body);
|
|
652
|
+
return {
|
|
653
|
+
kind: "lambda",
|
|
654
|
+
params: e.params.map(p => ({ name: p.name, type: p.ty })),
|
|
655
|
+
body: flat === null ? body : [{ kind: "return", value: flat }],
|
|
656
|
+
};
|
|
657
|
+
}
|
|
552
658
|
case "forall":
|
|
553
659
|
return { kind: "forall", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
554
660
|
case "exists":
|
|
555
661
|
return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
|
|
556
662
|
case "conditional": {
|
|
557
|
-
|
|
663
|
+
// JS truthiness coercion (string/array/int → ... > 0). Matches SPEC §3.1
|
|
664
|
+
// negation forms (`!s` → `s == ""`). Optional conds are already
|
|
665
|
+
// rewritten to someMatch by narrow.ts.
|
|
666
|
+
const cond = coerceCondToBool(lowerExpr(e.cond, binds), e.cond.ty);
|
|
558
667
|
let thenExpr = lowerExpr(e.then, binds);
|
|
559
668
|
let elseExpr = lowerExpr(e.else, binds);
|
|
560
669
|
if (e.ty.kind === "optional") {
|
|
@@ -586,7 +695,11 @@ function lowerExpr(e, binds) {
|
|
|
586
695
|
// path with the binder pre-lowering.
|
|
587
696
|
const replaced = replacePathInTExpr(e.someBody, path, e.binder, e.binderTy);
|
|
588
697
|
someBody = lowerExpr(replaced, binds);
|
|
589
|
-
|
|
698
|
+
// Bare-var shortcut, but route \result through lowerExpr so the
|
|
699
|
+
// lemma-side replaceVar pass can substitute it with the function call.
|
|
700
|
+
scrutinee = path.fields.length === 0 && path.rootVar !== "\\result"
|
|
701
|
+
? path.rootVar
|
|
702
|
+
: lowerExpr(e.scrutinee, binds);
|
|
590
703
|
}
|
|
591
704
|
else {
|
|
592
705
|
// Complex scrutinee — narrow pre-bound the someBody to use the binder directly,
|
|
@@ -607,11 +720,48 @@ function lowerExpr(e, binds) {
|
|
|
607
720
|
],
|
|
608
721
|
};
|
|
609
722
|
}
|
|
610
|
-
case "tagMatch":
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
723
|
+
case "tagMatch": {
|
|
724
|
+
// Expression-form tagMatch — emitted by `ruleImplArrayIsArray` for spec
|
|
725
|
+
// implications like `Array.isArray(x) ==> B` and by
|
|
726
|
+
// `ruleConditionalArrayIsArray` for ternary narrowing. Substitutes
|
|
727
|
+
// scrutinee field accesses and (for synth array-unions) scrutinee
|
|
728
|
+
// path occurrences inside each arm with the variant's payload binder.
|
|
729
|
+
// Path scrutinees (e.g. `m.content`) get a synthesized hint derived
|
|
730
|
+
// from the last field/var name so the binder reads naturally.
|
|
731
|
+
const scrutinee = lowerExpr(e.scrutinee, binds);
|
|
732
|
+
const decl = _typeDecls.find(d => d.name === e.typeName);
|
|
733
|
+
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
734
|
+
const varName = e.scrutinee.kind === "var" ? e.scrutinee.name : undefined;
|
|
735
|
+
const pathHint = varName ?? scrutineeHint(e.scrutinee);
|
|
736
|
+
const wrapOpt = e.ty.kind === "optional";
|
|
737
|
+
const arms = e.cases.map(c => {
|
|
738
|
+
const variant = decl?.variants?.find(v => v.name === c.variant);
|
|
739
|
+
const fields = variant?.fields ?? [];
|
|
740
|
+
let body = lowerExpr(c.body, binds);
|
|
741
|
+
if (varName && fields.length > 0) {
|
|
742
|
+
body = replaceFieldAccess(body, varName, fields);
|
|
743
|
+
if (isSynthArrayUnion && fields.length === 1) {
|
|
744
|
+
body = replaceVarInExpr(body, varName, matchBinder(fields[0].name, varName));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else if (!varName && isSynthArrayUnion && fields.length === 1) {
|
|
748
|
+
// Path scrutinee (e.g. `m.content`): replace structural occurrences
|
|
749
|
+
// with the binder var ref.
|
|
750
|
+
const binderName = matchBinder(fields[0].name, pathHint);
|
|
751
|
+
body = replaceExprInExpr(body, scrutinee, { kind: "var", name: binderName });
|
|
752
|
+
}
|
|
753
|
+
if (wrapOpt)
|
|
754
|
+
body = wrapOptionalBranch(body, c.body);
|
|
755
|
+
return { pattern: buildMatchPattern(c.variant, fields, pathHint), body };
|
|
756
|
+
});
|
|
757
|
+
if (e.fallthrough) {
|
|
758
|
+
let body = lowerExpr(e.fallthrough, binds);
|
|
759
|
+
if (wrapOpt)
|
|
760
|
+
body = wrapOptionalBranch(body, e.fallthrough);
|
|
761
|
+
arms.push({ pattern: "_", body });
|
|
762
|
+
}
|
|
763
|
+
return { kind: "match", scrutinee: varName ?? scrutinee, arms };
|
|
764
|
+
}
|
|
615
765
|
}
|
|
616
766
|
}
|
|
617
767
|
function flattenImpl(e) {
|
|
@@ -665,7 +815,119 @@ function replaceFieldAccess(e, varName, fields) {
|
|
|
665
815
|
return null;
|
|
666
816
|
});
|
|
667
817
|
}
|
|
818
|
+
/** Replace bare `var(oldName)` references → `var(newName)` in lowered IR.
|
|
819
|
+
* Used inside synth array-union match arms: the user code refers to the
|
|
820
|
+
* scrutinee by its bare name (`content`), but in the arm body that name
|
|
821
|
+
* must refer to the variant's sole payload binder (`i_content_arr`). */
|
|
822
|
+
function replaceVarInExpr(e, oldName, newName) {
|
|
823
|
+
return mapExpr(e, x => {
|
|
824
|
+
if (x.kind === "var" && x.name === oldName)
|
|
825
|
+
return { kind: "var", name: newName };
|
|
826
|
+
// If a binding shadows the name, stop substituting inside its body.
|
|
827
|
+
if (x.kind === "let" && x.name === oldName)
|
|
828
|
+
return { ...x, value: replaceVarInExpr(x.value, oldName, newName) };
|
|
829
|
+
return null;
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
/** Structural equality on Expr access-paths (var / field chain). Enough to
|
|
833
|
+
* match the scrutinee `m.content` against later occurrences in a match arm. */
|
|
834
|
+
function exprPathEqual(a, b) {
|
|
835
|
+
if (a.kind !== b.kind)
|
|
836
|
+
return false;
|
|
837
|
+
if (a.kind === "var" && b.kind === "var")
|
|
838
|
+
return a.name === b.name;
|
|
839
|
+
if (a.kind === "field" && b.kind === "field")
|
|
840
|
+
return a.field === b.field && exprPathEqual(a.obj, b.obj);
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
/** Substitute every occurrence of `target` (an access-path Expr) with `repl`
|
|
844
|
+
* inside `e`. Mirror of `replaceVarInExpr` but keyed on a sub-path rather
|
|
845
|
+
* than a bare name — needed when the narrowing scrutinee is `m.content`
|
|
846
|
+
* (field access) rather than a bare `content` (var). */
|
|
847
|
+
function replaceExprInExpr(e, target, repl) {
|
|
848
|
+
return mapExpr(e, x => {
|
|
849
|
+
if (exprPathEqual(x, target))
|
|
850
|
+
return repl;
|
|
851
|
+
return null;
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
/** Extract a short reader-friendly hint for a TExpr access-path: the last
|
|
855
|
+
* field name in a field chain, or the var name. Used to derive a stable
|
|
856
|
+
* binder prefix when the scrutinee isn't a bare var. */
|
|
857
|
+
function scrutineeHint(e) {
|
|
858
|
+
if (e.kind === "var")
|
|
859
|
+
return e.name;
|
|
860
|
+
if (e.kind === "field")
|
|
861
|
+
return e.field;
|
|
862
|
+
return "x";
|
|
863
|
+
}
|
|
668
864
|
// ── Transform statements ─────────────────────────────────────
|
|
865
|
+
// `if (X) continue; rest` → `if (!X) { rest }` at the top of a loop body.
|
|
866
|
+
// Dafny's lowered while-loops have the index increment at the bottom, so a
|
|
867
|
+
// `continue` would skip it and loop forever; rewriting to if/else lets the
|
|
868
|
+
// loop fall through normally.
|
|
869
|
+
function negateExpr(e) {
|
|
870
|
+
if (e.kind === "unop" && e.op === "!")
|
|
871
|
+
return e.expr;
|
|
872
|
+
return { kind: "unop", op: "!", expr: e };
|
|
873
|
+
}
|
|
874
|
+
/** Build the two pieces of an `arr.pop()` lowering on a named array variable:
|
|
875
|
+
* - `optValue` is `(if |arr|>0 then Some(arr[|arr|-1]) else None)` (the popped element)
|
|
876
|
+
* - `guardedTrunc` is `(if |arr|>0 then arr[..|arr|-1] else arr)` (the array minus its last element)
|
|
877
|
+
* Callers wrap these in let/assign statements appropriate to their context. */
|
|
878
|
+
function buildPopLowering(arrName, arrTy) {
|
|
879
|
+
const arrVar = { kind: "var", name: arrName };
|
|
880
|
+
const arrLen = { kind: "field", obj: arrVar, field: "size" };
|
|
881
|
+
const lastIdx = { kind: "binop", op: "-", left: arrLen, right: { kind: "num", value: 1 } };
|
|
882
|
+
const lastElem = { kind: "index", arr: arrVar, idx: lastIdx };
|
|
883
|
+
const isNonEmpty = { kind: "binop", op: ">", left: arrLen, right: { kind: "num", value: 0 } };
|
|
884
|
+
const optValue = { kind: "if", cond: isNonEmpty,
|
|
885
|
+
then: { kind: "app", fn: "Some", args: [lastElem] },
|
|
886
|
+
else: { kind: "var", name: "undefined" } };
|
|
887
|
+
const truncated = { kind: "methodCall", obj: arrVar, objTy: arrTy, method: "slice",
|
|
888
|
+
args: [{ kind: "num", value: 0 }, lastIdx], monadic: false };
|
|
889
|
+
const guardedTrunc = { kind: "if", cond: isNonEmpty, then: truncated, else: arrVar };
|
|
890
|
+
return { optValue, guardedTrunc };
|
|
891
|
+
}
|
|
892
|
+
function eliminateTopLevelContinue(stmts) {
|
|
893
|
+
const out = [];
|
|
894
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
895
|
+
const s = stmts[i];
|
|
896
|
+
// `if (X) {...A, continue}; rest` (empty else, trailing continue in then)
|
|
897
|
+
// — if A is empty, rewrite to `if (!X) { rest }`; otherwise rewrite to
|
|
898
|
+
// `if (X) { ...A } else { rest }`. Either form lets the loop fall through
|
|
899
|
+
// naturally past the bottom of the body.
|
|
900
|
+
if (s.kind === "if" && s.else.length === 0 &&
|
|
901
|
+
s.then.length >= 1 && s.then[s.then.length - 1].kind === "continue") {
|
|
902
|
+
const rest = eliminateTopLevelContinue(stmts.slice(i + 1));
|
|
903
|
+
const thenWithoutContinue = s.then.slice(0, -1);
|
|
904
|
+
if (thenWithoutContinue.length === 0) {
|
|
905
|
+
out.push({ kind: "if", cond: negateExpr(s.cond), then: rest, else: [] });
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
out.push({ kind: "if", cond: s.cond, then: thenWithoutContinue, else: rest });
|
|
909
|
+
}
|
|
910
|
+
return out;
|
|
911
|
+
}
|
|
912
|
+
// narrow.ts's ruleEarlyReturnConsume rewrites `if (!x) continue; rest`
|
|
913
|
+
// (when x is Optional) to a someMatch which transform.ts then emits as a
|
|
914
|
+
// `match`. A trailing `continue` inside a match arm is a no-op when the
|
|
915
|
+
// match is the last statement in the loop body — drop it.
|
|
916
|
+
if (s.kind === "match" && i === stmts.length - 1) {
|
|
917
|
+
const arms = s.arms.map(arm => {
|
|
918
|
+
const b = arm.body;
|
|
919
|
+
if (b.length > 0 && b[b.length - 1].kind === "continue") {
|
|
920
|
+
return { ...arm, body: b.slice(0, -1) };
|
|
921
|
+
}
|
|
922
|
+
return arm;
|
|
923
|
+
});
|
|
924
|
+
out.push({ ...s, arms });
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
out.push(s);
|
|
928
|
+
}
|
|
929
|
+
return out;
|
|
930
|
+
}
|
|
669
931
|
function transformStmts(stmts, typeDecls) {
|
|
670
932
|
const result = [];
|
|
671
933
|
let i = 0;
|
|
@@ -690,7 +952,7 @@ function transformStmts(stmts, typeDecls) {
|
|
|
690
952
|
const idxName = `_${keyName}_idx${suffix}`;
|
|
691
953
|
const idx = { kind: "var", name: idxName };
|
|
692
954
|
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
693
|
-
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
955
|
+
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
694
956
|
const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
|
|
695
957
|
const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
|
|
696
958
|
result.push({
|
|
@@ -716,7 +978,7 @@ function transformStmts(stmts, typeDecls) {
|
|
|
716
978
|
const idxName = `_${keyName}_idx${suffix}`;
|
|
717
979
|
const idx = { kind: "var", name: idxName };
|
|
718
980
|
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
719
|
-
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
981
|
+
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
720
982
|
const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
|
|
721
983
|
const letVal = { kind: "let", name: valueName, type: valueTy, mutable: false,
|
|
722
984
|
value: { kind: "methodCall", obj: iterExpr, objTy: s.iterable.ty, method: "getDirect", args: [{ kind: "var", name: keyName }], monadic: false } };
|
|
@@ -743,7 +1005,7 @@ function transformStmts(stmts, typeDecls) {
|
|
|
743
1005
|
const idxName = `_${varName}_idx${suffix}`;
|
|
744
1006
|
const idx = { kind: "var", name: idxName };
|
|
745
1007
|
const arrSize = { kind: "field", obj: iterExpr, field: "size" };
|
|
746
|
-
const bodyStmts = transformStmts(s.body, typeDecls);
|
|
1008
|
+
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
747
1009
|
const letElem = { kind: "let", name: varName, type: varTy, mutable: false, value: { kind: "index", arr: iterExpr, idx } };
|
|
748
1010
|
// Auto-add bound invariant: idx ≤ bound (always true for range loops)
|
|
749
1011
|
const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
|
|
@@ -788,6 +1050,17 @@ function transformStmt(s, typeDecls) {
|
|
|
788
1050
|
return [letHead, sliceTail];
|
|
789
1051
|
}
|
|
790
1052
|
}
|
|
1053
|
+
// let x = arr.pop() → let x: T? = (Option-expr); arr := (truncated-or-self)
|
|
1054
|
+
if (init && init.fn.kind === "field" && init.fn.field === "pop" && init.fn.obj.ty.kind === "array") {
|
|
1055
|
+
const arrName = init.fn.obj.kind === "var" ? init.fn.obj.name : undefined;
|
|
1056
|
+
if (arrName) {
|
|
1057
|
+
const { optValue, guardedTrunc } = buildPopLowering(arrName, init.fn.obj.ty);
|
|
1058
|
+
return [
|
|
1059
|
+
{ kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: optValue },
|
|
1060
|
+
{ kind: "assign", target: arrName, value: guardedTrunc },
|
|
1061
|
+
];
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
791
1064
|
// new Map(arr.map(n => [n.field, n])) → let m = map[]; for (n of arr) m[n.field] := n
|
|
792
1065
|
if (init && init.fn.kind === "var" && init.fn.name === "__mapFromArray" &&
|
|
793
1066
|
init.args.length === 1 && init.args[0].kind === "call" &&
|
|
@@ -838,6 +1111,17 @@ function transformStmt(s, typeDecls) {
|
|
|
838
1111
|
return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
|
|
839
1112
|
}
|
|
840
1113
|
case "assign": {
|
|
1114
|
+
// x = arr.pop() → x := (Option-expr); arr := (truncated-or-self)
|
|
1115
|
+
if (s.value.kind === "call" && s.value.fn.kind === "field" &&
|
|
1116
|
+
s.value.fn.field === "pop" && s.value.fn.obj.ty.kind === "array" &&
|
|
1117
|
+
s.value.fn.obj.kind === "var") {
|
|
1118
|
+
const arrName = s.value.fn.obj.name;
|
|
1119
|
+
const { optValue, guardedTrunc } = buildPopLowering(arrName, s.value.fn.obj.ty);
|
|
1120
|
+
return [
|
|
1121
|
+
{ kind: "assign", target: s.target, value: optValue },
|
|
1122
|
+
{ kind: "assign", target: arrName, value: guardedTrunc },
|
|
1123
|
+
];
|
|
1124
|
+
}
|
|
841
1125
|
// Top-level method call → direct monadic bind, no lifting needed
|
|
842
1126
|
if (s.value.kind === "call" && s.value.callKind === "method")
|
|
843
1127
|
return [{ kind: "bind", target: s.target, value: transformExpr(s.value) }];
|
|
@@ -892,16 +1176,16 @@ function transformStmt(s, typeDecls) {
|
|
|
892
1176
|
case "if": {
|
|
893
1177
|
// Lift from condition only (Lean rule: don't lift from branches).
|
|
894
1178
|
const { binds, expr: cond } = liftMethodCalls(s.cond);
|
|
895
|
-
return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
1179
|
+
return [...binds, { kind: "if", cond: coerceCondToBool(cond, s.cond.ty), then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
896
1180
|
}
|
|
897
1181
|
case "while":
|
|
898
1182
|
return [{
|
|
899
1183
|
kind: "while",
|
|
900
|
-
cond: transformExpr(s.cond),
|
|
1184
|
+
cond: coerceCondToBool(transformExpr(s.cond), s.cond.ty),
|
|
901
1185
|
invariants: s.invariants.map(transformExpr),
|
|
902
1186
|
decreasing: s.decreases ? transformExpr(s.decreases) : null,
|
|
903
1187
|
doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
|
|
904
|
-
body: transformStmts(s.body, typeDecls),
|
|
1188
|
+
body: eliminateTopLevelContinue(transformStmts(s.body, typeDecls)),
|
|
905
1189
|
}];
|
|
906
1190
|
case "throw":
|
|
907
1191
|
return [{ kind: "assert", expr: { kind: "bool", value: false } }];
|
|
@@ -914,7 +1198,7 @@ function transformStmt(s, typeDecls) {
|
|
|
914
1198
|
case "ghostAssign":
|
|
915
1199
|
return [{ kind: "ghostAssign", target: s.target, value: transformExpr(s.value) }];
|
|
916
1200
|
case "assert":
|
|
917
|
-
return [{ kind: "assert", expr: transformExpr(s.expr) }];
|
|
1201
|
+
return [{ kind: "assert", expr: transformExpr(s.expr), assumed: s.assumed }];
|
|
918
1202
|
case "someMatch": {
|
|
919
1203
|
const path = asTAccessPath(s.scrutinee);
|
|
920
1204
|
if (path) {
|
|
@@ -932,11 +1216,8 @@ function transformStmt(s, typeDecls) {
|
|
|
932
1216
|
}
|
|
933
1217
|
throw new Error(`someMatch stmt scrutinee must be a pure access path, got ${s.scrutinee.kind}`);
|
|
934
1218
|
}
|
|
935
|
-
case "tagMatch":
|
|
936
|
-
|
|
937
|
-
const chain = { varName, typeName: s.typeName, cases: s.cases, fallthrough: s.fallthrough };
|
|
938
|
-
return [emitMatchStmt(chain, typeDecls)];
|
|
939
|
-
}
|
|
1219
|
+
case "tagMatch":
|
|
1220
|
+
return [emitMatchStmt(s.scrutinee, s.typeName, s.cases, s.fallthrough, typeDecls)];
|
|
940
1221
|
}
|
|
941
1222
|
}
|
|
942
1223
|
/** Apply an expression transform to all expressions in a statement (convenience wrapper). */
|
|
@@ -961,30 +1242,80 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
|
961
1242
|
}
|
|
962
1243
|
return arms;
|
|
963
1244
|
}
|
|
964
|
-
function emitMatchStmt(
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1245
|
+
function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
|
|
1246
|
+
const decl = typeDecls.find(d => d.name === typeName);
|
|
1247
|
+
// Synth array-unions (discriminant "__isArray__") have single-field variants
|
|
1248
|
+
// ArrayBranch(arr) / NonArrayBranch(val). The matched arm refers to the
|
|
1249
|
+
// scrutinee by its bare name/path (`content`, `m.content`), not `.arr`, so
|
|
1250
|
+
// we substitute that whole reference with the variant's sole field binder.
|
|
1251
|
+
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
1252
|
+
// The scrutinee is a bare var (`current`) or a field-access path
|
|
1253
|
+
// (`current.content`). `prefix` names the binder scope — the var name, or a
|
|
1254
|
+
// safe id derived from the path (`current.content` → `current_content`) —
|
|
1255
|
+
// and is used for both pattern binders and arm-body substitution so they
|
|
1256
|
+
// always agree. A var scrutinee has empty `fields`, so `prefix` is just its
|
|
1257
|
+
// name and the emitted code is unchanged from before this generalization.
|
|
1258
|
+
const path = asTAccessPath(scrutinee);
|
|
1259
|
+
const isPath = !!path && path.fields.length > 0;
|
|
1260
|
+
const prefix = path ? [path.rootVar, ...path.fields].join("_") : "?";
|
|
1261
|
+
function transformArmBody(body, fields) {
|
|
1262
|
+
let stmts;
|
|
1263
|
+
if (isPath && path) {
|
|
1264
|
+
// Path scrutinee: the matched value is referred to by the bare path, so
|
|
1265
|
+
// substitute the whole path (only the synth single-field shape arises
|
|
1266
|
+
// here — discriminant chains require a var scrutinee).
|
|
1267
|
+
stmts = isSynthArrayUnion && fields.length === 1
|
|
1268
|
+
? replacePathInTStmts(body, path, matchBinder(fields[0].name, prefix), fields[0].type ?? parseTsType(fields[0].tsType))
|
|
1269
|
+
: body;
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
stmts = replaceFieldAccessInTStmts(body, prefix, fields);
|
|
1273
|
+
if (isSynthArrayUnion && fields.length === 1) {
|
|
1274
|
+
const f = fields[0];
|
|
1275
|
+
stmts = replaceVarInTStmts(stmts, prefix, matchBinder(f.name, prefix), f.type ?? parseTsType(f.tsType));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return transformStmts(stmts, typeDecls);
|
|
1279
|
+
}
|
|
1280
|
+
const armCases = cases.map(c => ({ name: c.variant, body: c.body }));
|
|
1281
|
+
const arms = buildMatchArms(armCases, prefix, typeName, typeDecls, (body, _vn, fields) => transformArmBody(body, fields));
|
|
1282
|
+
// Add the fallthrough arm whenever the listed cases don't cover every
|
|
1283
|
+
// variant — needed for exhaustiveness even when there's no `else`
|
|
1284
|
+
// (`fallthrough` empty), e.g. `if (Array.isArray(x)) {...}` with no else
|
|
1285
|
+
// becomes `match x { case ArrayBranch(..) => ... case NonArrayBranch(..) => }`.
|
|
1286
|
+
const allCovered = !!decl?.variants && cases.length >= decl.variants.length;
|
|
1287
|
+
if (!allCovered) {
|
|
1288
|
+
const remaining = remainingVariant(typeName, cases, typeDecls);
|
|
969
1289
|
if (remaining) {
|
|
970
1290
|
// Exactly one variant left — destructure so the fallthrough body can
|
|
971
1291
|
// access variant-specific fields (Lean requires this; Dafny tolerates `_`).
|
|
972
|
-
const pattern = buildMatchPattern(remaining.name, remaining.fields,
|
|
973
|
-
const body =
|
|
1292
|
+
const pattern = buildMatchPattern(remaining.name, remaining.fields, prefix);
|
|
1293
|
+
const body = transformArmBody(fallthrough, remaining.fields);
|
|
974
1294
|
arms.push({ pattern, body });
|
|
975
1295
|
}
|
|
976
1296
|
else {
|
|
977
|
-
arms.push({ pattern: "_", body: transformStmts(
|
|
1297
|
+
arms.push({ pattern: "_", body: transformStmts(fallthrough, typeDecls) });
|
|
978
1298
|
}
|
|
979
1299
|
}
|
|
980
|
-
return { kind: "match", scrutinee:
|
|
1300
|
+
return { kind: "match", scrutinee: isPath ? transformExpr(scrutinee) : prefix, arms };
|
|
1301
|
+
}
|
|
1302
|
+
/** Replace bare `var(oldName)` references → `var(newName)` with the given type.
|
|
1303
|
+
* Used by emitMatchStmt for synth array-unions where the variant has a single
|
|
1304
|
+
* payload field and the user code refers to the scrutinee by its bare name. */
|
|
1305
|
+
function replaceVarInTStmts(stmts, oldName, newName, newTy) {
|
|
1306
|
+
return stmts.map(s => mapTStmt(s, e => {
|
|
1307
|
+
if (e.kind === "var" && e.name === oldName) {
|
|
1308
|
+
return { kind: "var", name: newName, ty: newTy };
|
|
1309
|
+
}
|
|
1310
|
+
return null;
|
|
1311
|
+
}));
|
|
981
1312
|
}
|
|
982
1313
|
/** If the chain has matched all variants but one, return that remaining variant. */
|
|
983
|
-
function remainingVariant(
|
|
984
|
-
const decl = typeDecls.find(d => d.name ===
|
|
1314
|
+
function remainingVariant(typeName, cases, typeDecls) {
|
|
1315
|
+
const decl = typeDecls.find(d => d.name === typeName);
|
|
985
1316
|
if (!decl?.variants)
|
|
986
1317
|
return null;
|
|
987
|
-
const matched = new Set(
|
|
1318
|
+
const matched = new Set(cases.map(c => c.variant));
|
|
988
1319
|
const remaining = decl.variants.filter(v => !matched.has(v.name));
|
|
989
1320
|
if (remaining.length !== 1)
|
|
990
1321
|
return null;
|
|
@@ -1168,12 +1499,20 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
1168
1499
|
}
|
|
1169
1500
|
function transformPureMatch(chain, typeDecls) {
|
|
1170
1501
|
const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
|
|
1502
|
+
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
1503
|
+
// Synth array-unions have single-field variants and user code refers to the
|
|
1504
|
+
// scrutinee by its bare name, not field-accessed. See emitMatchStmt for
|
|
1505
|
+
// the statement-level counterpart of this substitution.
|
|
1506
|
+
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
1171
1507
|
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => {
|
|
1172
1508
|
let result = transformPureBody(body, typeDecls);
|
|
1173
1509
|
if (!result)
|
|
1174
1510
|
return null;
|
|
1175
1511
|
if (fields.length > 0 && vn)
|
|
1176
1512
|
result = replaceFieldAccess(result, vn, fields);
|
|
1513
|
+
if (isSynthArrayUnion && fields.length === 1 && vn) {
|
|
1514
|
+
result = replaceVarInExpr(result, vn, matchBinder(fields[0].name, vn));
|
|
1515
|
+
}
|
|
1177
1516
|
return result;
|
|
1178
1517
|
});
|
|
1179
1518
|
if (!arms)
|
|
@@ -1181,10 +1520,9 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
1181
1520
|
// Idiomatic TS often has an unreachable fallthrough after exhaustive if-chains on
|
|
1182
1521
|
// discriminated unions. Skip the catch-all arm when all variants are matched,
|
|
1183
1522
|
// since Lean errors on redundant match arms.
|
|
1184
|
-
const decl = typeDecls.find(d => d.name === chain.typeName);
|
|
1185
1523
|
const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
|
|
1186
1524
|
if (chain.fallthrough.length > 0 && !allCovered) {
|
|
1187
|
-
const remaining = remainingVariant(chain, typeDecls);
|
|
1525
|
+
const remaining = remainingVariant(chain.typeName, chain.cases, typeDecls);
|
|
1188
1526
|
if (remaining) {
|
|
1189
1527
|
// Exactly one variant left — destructure for variant-specific field access.
|
|
1190
1528
|
let body = transformPureBody(chain.fallthrough, typeDecls);
|
|
@@ -1192,6 +1530,9 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
1192
1530
|
return null;
|
|
1193
1531
|
if (remaining.fields.length > 0)
|
|
1194
1532
|
body = replaceFieldAccess(body, chain.varName, remaining.fields);
|
|
1533
|
+
if (isSynthArrayUnion && remaining.fields.length === 1) {
|
|
1534
|
+
body = replaceVarInExpr(body, chain.varName, matchBinder(remaining.fields[0].name, chain.varName));
|
|
1535
|
+
}
|
|
1195
1536
|
arms.push({ pattern: buildMatchPattern(remaining.name, remaining.fields, chain.varName), body });
|
|
1196
1537
|
}
|
|
1197
1538
|
else {
|
|
@@ -1350,9 +1691,9 @@ export function transformModule(mod, specImport) {
|
|
|
1350
1691
|
continue;
|
|
1351
1692
|
const body = transformPureBody(fn.body, mod.typeDecls);
|
|
1352
1693
|
if (body) {
|
|
1353
|
-
// For
|
|
1694
|
+
// For pure-function lemmas, replace \result with the function call.
|
|
1354
1695
|
const fnCall = { kind: "app", fn: fn.name, args: fn.params.map(p => ({ kind: "var", name: p.name })) };
|
|
1355
|
-
const ensures = fn.ensures.map(e => replaceVar(transformExpr(e), "
|
|
1696
|
+
const ensures = fn.ensures.map(e => replaceVar(transformExpr(e), "\\result", fnCall));
|
|
1356
1697
|
pureDefs.push({
|
|
1357
1698
|
kind: "def",
|
|
1358
1699
|
name: fn.name,
|
|
@@ -1383,18 +1724,35 @@ export function transformModule(mod, specImport) {
|
|
|
1383
1724
|
}
|
|
1384
1725
|
}
|
|
1385
1726
|
const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
|
|
1727
|
+
// Externs: emit as top-of-file `function {:axiom}` (Dafny) declarations.
|
|
1728
|
+
// Any `requires`/`ensures` from the source declaration come along so callers
|
|
1729
|
+
// see the same spec the source itself verified. Substitute `\result` with the
|
|
1730
|
+
// function call (same pattern as for in-file pure-function ensures).
|
|
1731
|
+
const externDecls = (mod.externs ?? []).map(ext => {
|
|
1732
|
+
const fnCall = { kind: "app", fn: ext.flat, args: ext.params.map(p => ({ kind: "var", name: p.name })) };
|
|
1733
|
+
return {
|
|
1734
|
+
kind: "extern",
|
|
1735
|
+
name: ext.flat,
|
|
1736
|
+
typeParams: ext.typeParams,
|
|
1737
|
+
params: ext.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1738
|
+
returnType: ext.returnTy,
|
|
1739
|
+
requires: ext.requires.map(transformExpr),
|
|
1740
|
+
ensures: ext.ensures.map(e => replaceVar(transformExpr(e), "\\result", fnCall)),
|
|
1741
|
+
};
|
|
1742
|
+
});
|
|
1386
1743
|
// Types file
|
|
1387
1744
|
const typesImports = ["LemmaScript"];
|
|
1388
1745
|
let typesFile = null;
|
|
1389
1746
|
const pureNamespace = pureDefs.length > 0
|
|
1390
1747
|
? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
|
|
1391
1748
|
: [];
|
|
1392
|
-
if (typeDecls.length > 0 || pureDefs.length > 0) {
|
|
1749
|
+
if (typeDecls.length > 0 || pureDefs.length > 0 || externDecls.length > 0) {
|
|
1393
1750
|
typesFile = {
|
|
1394
1751
|
comment: " Generated by lsc — Lean types and pure function mirrors.",
|
|
1395
1752
|
imports: typesImports,
|
|
1396
1753
|
options: [],
|
|
1397
|
-
|
|
1754
|
+
// Externs come first so they're in scope for every later declaration.
|
|
1755
|
+
decls: [...externDecls, ...typeDecls, ...pureNamespace],
|
|
1398
1756
|
};
|
|
1399
1757
|
}
|
|
1400
1758
|
// Def file: Velvet methods
|