lemmascript 0.5.13 → 0.5.15
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 -2
- package/tools/dist/dafny-emit.js +117 -48
- package/tools/dist/ir.js +40 -3
- package/tools/dist/lean-emit.js +29 -9
- package/tools/dist/lsc.js +2 -1
- package/tools/dist/names.js +5 -0
- package/tools/dist/narrow.js +49 -71
- package/tools/dist/peephole.js +8 -9
- package/tools/dist/transform.js +41 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lemmascript",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.15",
|
|
4
4
|
"description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"type": "git",
|
|
33
33
|
"url": "https://github.com/midspiral/LemmaScript"
|
|
34
34
|
},
|
|
35
|
-
"homepage": "https://lemmascript.
|
|
35
|
+
"homepage": "https://lemmascript.org",
|
|
36
36
|
"keywords": [
|
|
37
37
|
"lemmascript",
|
|
38
38
|
"verification",
|
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Dafny emitter — IR → Dafny text.
|
|
3
3
|
*/
|
|
4
4
|
import { usesName, usesNameInDecl } from "./ir.js";
|
|
5
|
-
import { freshName } from "./names.js";
|
|
5
|
+
import { freshName, userNames } from "./names.js";
|
|
6
6
|
import { renameFreeVar } from "./transform.js";
|
|
7
7
|
/** Fresh binder for a comprehension wrapping the given subexpressions: `base`
|
|
8
8
|
* verbatim unless one of them references it, then primed until free. A *local*
|
|
@@ -42,7 +42,7 @@ function tyToDafny(ty) {
|
|
|
42
42
|
needPreamble("OptionType");
|
|
43
43
|
return `Option<${tyToDafny(ty.inner)}>`;
|
|
44
44
|
}
|
|
45
|
-
case "user": return ty.name;
|
|
45
|
+
case "user": return escapeName(ty.name);
|
|
46
46
|
case "fn": return `(${ty.params.map(tyToDafny).join(", ")}) -> ${tyToDafny(ty.result)}`;
|
|
47
47
|
// Out-of-subset (`any`/`unknown`); opaque so real ops on it fail loudly
|
|
48
48
|
// rather than silently verify as `int`. Mirrors the Lean backend's `_`.
|
|
@@ -75,28 +75,82 @@ const DAFNY_KEYWORDS = new Set([
|
|
|
75
75
|
"copredicate", "inductive",
|
|
76
76
|
]);
|
|
77
77
|
// The Dafny out-parameter name for the method currently being emitted. Default
|
|
78
|
-
// `res`, but bumped (e.g. `
|
|
79
|
-
// methodHeader and reset per decl. `\result` in an ensures must use the
|
|
80
|
-
// name, so escapeName routes it here.
|
|
78
|
+
// `res`, but bumped (e.g. `res'`) when the method's own scope uses `res` — set
|
|
79
|
+
// by methodHeader and reset per decl. `\result` in an ensures must use the
|
|
80
|
+
// *same* name, so escapeName routes it here.
|
|
81
81
|
let _resultName = "res";
|
|
82
|
+
// ── Dafny name allocation ──────────────────────────────────
|
|
83
|
+
//
|
|
84
|
+
// freshName (names.ts) freshens in the *raw TS* namespace — but that is not the
|
|
85
|
+
// namespace Dafny sees. Escaping maps `_x`→`i_x` and keyword `match`→`match_`,
|
|
86
|
+
// so two raw-distinct names can collapse *after* escaping: a raw-freshened temp
|
|
87
|
+
// `_t0'`→`i_t0'` colliding with a user `_t0` that escaped-and-primed to `i_t0'`.
|
|
88
|
+
// So Dafny hygiene is a second allocator, layered at emission: escape to a base,
|
|
89
|
+
// then freshen against the names already claimed in the Dafny namespace. User
|
|
90
|
+
// names are allocated up front (Dafny-safe ones kept exact); generated names are
|
|
91
|
+
// allocated on first sight and cached so a decl and its references agree. Reset
|
|
92
|
+
// per file. (The raw freshName layer stays — Lean has a different escaping story.)
|
|
93
|
+
function dafnyBaseName(name) {
|
|
94
|
+
if (DAFNY_KEYWORDS.has(name))
|
|
95
|
+
return `${name}_`;
|
|
96
|
+
if (name.startsWith("_"))
|
|
97
|
+
return `i${name}`; // Dafny forbids leading `_`
|
|
98
|
+
return name;
|
|
99
|
+
}
|
|
100
|
+
let _userDafnyNames = new Map();
|
|
101
|
+
let _generatedDafnyNames = new Map();
|
|
102
|
+
let _takenDafnyNames = new Set();
|
|
103
|
+
/** `base`, primed until free in the Dafny namespace. A prime can't occur in a
|
|
104
|
+
* TS identifier, so priming always leaves user-name space. */
|
|
105
|
+
function freshDafnyName(base) {
|
|
106
|
+
let out = base;
|
|
107
|
+
while (_takenDafnyNames.has(out))
|
|
108
|
+
out += "'";
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function resetDafnyNameCache() {
|
|
112
|
+
_userDafnyNames = new Map();
|
|
113
|
+
_generatedDafnyNames = new Map();
|
|
114
|
+
_takenDafnyNames = new Set();
|
|
115
|
+
const raws = [...userNames()].sort();
|
|
116
|
+
// Dafny-safe source names keep their spelling; names that must mangle are then
|
|
117
|
+
// freshened in the emitted namespace (safe-first, sorted → deterministic).
|
|
118
|
+
for (const raw of raws)
|
|
119
|
+
if (dafnyBaseName(raw) === raw) {
|
|
120
|
+
_userDafnyNames.set(raw, raw);
|
|
121
|
+
_takenDafnyNames.add(raw);
|
|
122
|
+
}
|
|
123
|
+
for (const raw of raws)
|
|
124
|
+
if (dafnyBaseName(raw) !== raw) {
|
|
125
|
+
const emitted = freshDafnyName(dafnyBaseName(raw));
|
|
126
|
+
_userDafnyNames.set(raw, emitted);
|
|
127
|
+
_takenDafnyNames.add(emitted);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
82
130
|
function escapeName(name) {
|
|
83
|
-
// \result is carried through the IR as
|
|
84
|
-
//
|
|
131
|
+
// \result is carried through the IR as var "\\result"; render it as the
|
|
132
|
+
// current method's out-parameter name (chosen locally by methodHeader).
|
|
85
133
|
if (name === "\\result")
|
|
86
134
|
return _resultName;
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
135
|
+
const user = _userDafnyNames.get(name);
|
|
136
|
+
if (user !== undefined)
|
|
137
|
+
return user;
|
|
138
|
+
return escapeGeneratedName(name);
|
|
139
|
+
}
|
|
140
|
+
/** Allocate a toolchain-generated name (an ANF temp, a comprehension binder, a
|
|
141
|
+
* companion `_ensures` lemma). Escapes to a base, then freshens in the Dafny
|
|
142
|
+
* namespace so it can't collapse onto an escaped user name. Bypasses the user
|
|
143
|
+
* map on purpose: the raw name is synthesized, so it must be freshened *away
|
|
144
|
+
* from* a same-spelled user name, not aliased onto it. Cached so a declaration
|
|
145
|
+
* and its references render identically. */
|
|
146
|
+
function escapeGeneratedName(name) {
|
|
147
|
+
const cached = _generatedDafnyNames.get(name);
|
|
148
|
+
if (cached !== undefined)
|
|
149
|
+
return cached;
|
|
150
|
+
const emitted = freshDafnyName(dafnyBaseName(name));
|
|
151
|
+
_generatedDafnyNames.set(name, emitted);
|
|
152
|
+
_takenDafnyNames.add(emitted);
|
|
153
|
+
return emitted;
|
|
100
154
|
}
|
|
101
155
|
/** Format a typed parameter list for Dafny: "x: int, y: seq<int>" */
|
|
102
156
|
function paramList(params) {
|
|
@@ -139,7 +193,7 @@ function emitQuantifier(e, keyword) {
|
|
|
139
193
|
while (body.kind === e.kind) {
|
|
140
194
|
const dty = tyToDafny(body.type);
|
|
141
195
|
const ann = dty === "string" ? "" : `: ${dty}`;
|
|
142
|
-
vars.push(`${body.var}${ann}`);
|
|
196
|
+
vars.push(`${escapeName(body.var)}${ann}`);
|
|
143
197
|
body = body.body;
|
|
144
198
|
}
|
|
145
199
|
return `${keyword} ${vars.join(", ")} :: ${emitExpr(body)}`;
|
|
@@ -337,7 +391,7 @@ function emitExpr(e) {
|
|
|
337
391
|
// Minted comprehension binder: freshen so a user variable `k` in the
|
|
338
392
|
// receiver or key isn't captured (`k != k` would delete nothing).
|
|
339
393
|
// Local check — only this comprehension's own operands can collide.
|
|
340
|
-
const k = freshBinder("k", e.obj, e.args[0]);
|
|
394
|
+
const k = escapeName(freshBinder("k", e.obj, e.args[0]));
|
|
341
395
|
return `(map ${k} | ${k} in ${obj} && ${k} != ${args[0]} :: ${obj}[${k}])`;
|
|
342
396
|
}
|
|
343
397
|
}
|
|
@@ -485,9 +539,11 @@ function emitExpr(e) {
|
|
|
485
539
|
}
|
|
486
540
|
case "field": {
|
|
487
541
|
const obj = emitExpr(e.obj);
|
|
488
|
-
|
|
542
|
+
// `size`/`length`/`keys` are collection intrinsics unless the transform
|
|
543
|
+
// proved this is a declared datatype field (then project it).
|
|
544
|
+
if (!e.datatypeField && (e.field === "size" || e.field === "length" || e.field === "collectionSize"))
|
|
489
545
|
return `|${obj}|`;
|
|
490
|
-
if (e.field === "keys")
|
|
546
|
+
if (!e.datatypeField && e.field === "keys")
|
|
491
547
|
return `${obj}.Keys`;
|
|
492
548
|
if (e.field === "toNat")
|
|
493
549
|
return obj;
|
|
@@ -719,23 +775,23 @@ function emitDecl(d) {
|
|
|
719
775
|
const fields = c.fields.map(f => collides.has(f.name) ? { ...f, name: `${f.name}_${c.name}` } : f);
|
|
720
776
|
return `${escapeName(c.name)}(${paramList(fields)})`;
|
|
721
777
|
});
|
|
722
|
-
return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
|
|
778
|
+
return `datatype ${escapeName(d.name)}${tp} = ${ctors.join(" | ")}`;
|
|
723
779
|
}
|
|
724
780
|
case "structure": {
|
|
725
781
|
const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
|
|
726
|
-
return `datatype ${d.name}${tp} = ${d.name}(${paramList(d.fields)})`;
|
|
782
|
+
return `datatype ${escapeName(d.name)}${tp} = ${escapeName(d.name)}(${paramList(d.fields)})`;
|
|
727
783
|
}
|
|
728
784
|
case "type-alias": {
|
|
729
|
-
return `type ${d.name} = ${tyToDafny(d.target)}`;
|
|
785
|
+
return `type ${escapeName(d.name)} = ${tyToDafny(d.target)}`;
|
|
730
786
|
}
|
|
731
787
|
case "opaque-type": {
|
|
732
788
|
// Abstract type — no definition. `(==)` so it can sit inside datatypes
|
|
733
789
|
// that derive structural equality. Never constructed or destructured.
|
|
734
|
-
return `type ${d.name}(==)`;
|
|
790
|
+
return `type ${escapeName(d.name)}(==)`;
|
|
735
791
|
}
|
|
736
792
|
case "def": {
|
|
737
793
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
738
|
-
const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
794
|
+
const lines = [`function ${escapeName(d.name)}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
739
795
|
for (const r of d.requires)
|
|
740
796
|
lines.push(` requires ${emitExpr(r)}`);
|
|
741
797
|
if (d.decreases)
|
|
@@ -748,7 +804,7 @@ function emitDecl(d) {
|
|
|
748
804
|
// Strip constraints like (==) from type params — ghost lemmas don't need them
|
|
749
805
|
const lemmaTP = d.typeParams.length > 0 ? `<${d.typeParams.map(t => t.replace(/\(.*\)/, '')).join(", ")}>` : "";
|
|
750
806
|
lines.push("");
|
|
751
|
-
lines.push(`lemma ${d.name}_ensures${lemmaTP}(${paramList(d.params)})`);
|
|
807
|
+
lines.push(`lemma ${escapeGeneratedName(`${d.name}_ensures`)}${lemmaTP}(${paramList(d.params)})`);
|
|
752
808
|
for (const r of d.requires)
|
|
753
809
|
lines.push(` requires ${emitExpr(r)}`);
|
|
754
810
|
for (const e of d.ensures)
|
|
@@ -760,7 +816,7 @@ function emitDecl(d) {
|
|
|
760
816
|
}
|
|
761
817
|
case "def-by-method": {
|
|
762
818
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
763
|
-
const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
819
|
+
const lines = [`function ${escapeName(d.name)}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
764
820
|
for (const r of d.requires)
|
|
765
821
|
lines.push(` requires ${emitExpr(r)}`);
|
|
766
822
|
if (d.decreases)
|
|
@@ -774,7 +830,7 @@ function emitDecl(d) {
|
|
|
774
830
|
}
|
|
775
831
|
case "method": {
|
|
776
832
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
777
|
-
const lines = [methodHeader(`method ${d.name}${tp}`, d.params, d.returnType, d)];
|
|
833
|
+
const lines = [methodHeader(`method ${escapeName(d.name)}${tp}`, d.params, d.returnType, d)];
|
|
778
834
|
for (const r of d.requires)
|
|
779
835
|
lines.push(` requires ${emitExpr(r)}`);
|
|
780
836
|
for (const e of d.ensures)
|
|
@@ -787,14 +843,14 @@ function emitDecl(d) {
|
|
|
787
843
|
return lines.join("\n");
|
|
788
844
|
}
|
|
789
845
|
case "class": {
|
|
790
|
-
const lines = [`class ${d.name} {`];
|
|
846
|
+
const lines = [`class ${escapeName(d.name)} {`];
|
|
791
847
|
for (const f of d.fields) {
|
|
792
848
|
lines.push(` var ${escapeName(f.name)}: ${tyToDafny(f.type)}`);
|
|
793
849
|
}
|
|
794
850
|
if (d.fields.length > 0 && d.methods.length > 0)
|
|
795
851
|
lines.push("");
|
|
796
852
|
for (const m of d.methods) {
|
|
797
|
-
lines.push(` ${methodHeader(`method ${m.name}`, m.params, m.returnType, m)}`);
|
|
853
|
+
lines.push(` ${methodHeader(`method ${escapeName(m.name)}`, m.params, m.returnType, m)}`);
|
|
798
854
|
for (const r of m.requires)
|
|
799
855
|
lines.push(` requires ${emitExpr(r)}`);
|
|
800
856
|
for (const e of m.ensures)
|
|
@@ -1027,22 +1083,39 @@ const SEQ_SORT_BY = `function {:axiom} SeqSortBy<T(==,!new)>(s: seq<T>, cmp: (T,
|
|
|
1027
1083
|
ensures multiset(SeqSortBy(s, cmp)) == multiset(s)
|
|
1028
1084
|
ensures |SeqSortBy(s, cmp)| == |s|
|
|
1029
1085
|
ensures forall i: int, j: int :: 0 <= i <= j < |SeqSortBy(s, cmp)| ==> cmp(SeqSortBy(s, cmp)[i], SeqSortBy(s, cmp)[j]) <= 0`;
|
|
1030
|
-
|
|
1086
|
+
// TS/JS String.prototype.trim() strips ECMAScript WhiteSpace ∪ LineTerminator:
|
|
1087
|
+
// Unicode general-category Zs plus TAB/VT/FF/CR/LF, LS/PS, and the BOM — NOT just
|
|
1088
|
+
// U+0020, and NOT U+0085 (NEL, which is Cc). See
|
|
1089
|
+
// https://tc39.es/ecma262/#sec-white-space and
|
|
1090
|
+
// https://tc39.es/ecma262/#sec-line-terminators.
|
|
1091
|
+
// `\\U{..}` are Dafny char escapes (not JS: the string
|
|
1092
|
+
// is emitted verbatim), so the enumeration below is Dafny source, not decoded.
|
|
1093
|
+
const STRING_TRIM = `predicate IsJSWhitespace(c: char)
|
|
1094
|
+
{
|
|
1095
|
+
c == '\\U{0009}' || c == '\\U{000A}' || c == '\\U{000B}' || c == '\\U{000C}' || c == '\\U{000D}' ||
|
|
1096
|
+
c == '\\U{0020}' || c == '\\U{00A0}' || c == '\\U{1680}' ||
|
|
1097
|
+
('\\U{2000}' <= c <= '\\U{200A}') ||
|
|
1098
|
+
c == '\\U{2028}' || c == '\\U{2029}' || c == '\\U{202F}' || c == '\\U{205F}' ||
|
|
1099
|
+
c == '\\U{3000}' || c == '\\U{FEFF}'
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function StringTrimLeft(s: string): string
|
|
1031
1103
|
ensures |StringTrimLeft(s)| <= |s|
|
|
1032
|
-
ensures StringTrimLeft(s) == "" || (|StringTrimLeft(s)| > 0 && StringTrimLeft(s)[0]
|
|
1104
|
+
ensures StringTrimLeft(s) == "" || (|StringTrimLeft(s)| > 0 && !IsJSWhitespace(StringTrimLeft(s)[0]))
|
|
1033
1105
|
decreases |s|
|
|
1034
1106
|
{
|
|
1035
1107
|
if |s| == 0 then ""
|
|
1036
|
-
else if s[0]
|
|
1108
|
+
else if IsJSWhitespace(s[0]) then StringTrimLeft(s[1..])
|
|
1037
1109
|
else s
|
|
1038
1110
|
}
|
|
1039
1111
|
|
|
1040
1112
|
function StringTrimRight(s: string): string
|
|
1041
1113
|
ensures |StringTrimRight(s)| <= |s|
|
|
1114
|
+
ensures StringTrimRight(s) == "" || (|StringTrimRight(s)| > 0 && !IsJSWhitespace(StringTrimRight(s)[|StringTrimRight(s)|-1]))
|
|
1042
1115
|
decreases |s|
|
|
1043
1116
|
{
|
|
1044
1117
|
if |s| == 0 then ""
|
|
1045
|
-
else if s[|s|-1]
|
|
1118
|
+
else if IsJSWhitespace(s[|s|-1]) then StringTrimRight(s[..|s|-1])
|
|
1046
1119
|
else s
|
|
1047
1120
|
}
|
|
1048
1121
|
|
|
@@ -1249,21 +1322,17 @@ function qualifyCtor(name, type) {
|
|
|
1249
1322
|
* "_" → "_"
|
|
1250
1323
|
*/
|
|
1251
1324
|
const CTOR_MAP = { "some": "Some", "none": "None" };
|
|
1252
|
-
function translatePattern(
|
|
1253
|
-
if (
|
|
1325
|
+
function translatePattern(p) {
|
|
1326
|
+
if (p.kind === "wild")
|
|
1254
1327
|
return "_";
|
|
1255
|
-
const
|
|
1256
|
-
if (
|
|
1257
|
-
return pattern;
|
|
1258
|
-
const ctorName = CTOR_MAP[m[1]] ?? escapeName(m[1]);
|
|
1259
|
-
const fields = m[2].trim();
|
|
1260
|
-
if (!fields)
|
|
1328
|
+
const ctorName = CTOR_MAP[p.ctor] ?? escapeName(p.ctor);
|
|
1329
|
+
if (p.binders.length === 0)
|
|
1261
1330
|
return ctorName;
|
|
1262
|
-
|
|
1263
|
-
return `${ctorName}(${fieldNames.join(", ")})`;
|
|
1331
|
+
return `${ctorName}(${p.binders.map(escapeName).join(", ")})`;
|
|
1264
1332
|
}
|
|
1265
1333
|
export function emitDafnyFile(file, tsFileName, opts) {
|
|
1266
1334
|
_useSafeSlice = !!opts?.safeSlice;
|
|
1335
|
+
resetDafnyNameCache();
|
|
1267
1336
|
buildRecordCtorMap(file.decls);
|
|
1268
1337
|
_neededPreambles.clear();
|
|
1269
1338
|
// Track successfully emitted pure defs — method wrappers are only
|
package/tools/dist/ir.js
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* The transform phase produces these types.
|
|
5
5
|
* The emit phase pretty-prints them to backend syntax (Lean or Dafny).
|
|
6
6
|
*/
|
|
7
|
+
export const pWild = () => ({ kind: "wild" });
|
|
8
|
+
export const pCtor = (ctor, ...binders) => ({ kind: "ctor", ctor, binders });
|
|
9
|
+
/** Binder identifiers a pattern introduces (`[]` for wildcard / nullary ctor). */
|
|
10
|
+
export function patternBinders(p) {
|
|
11
|
+
return p.kind === "ctor" ? p.binders : [];
|
|
12
|
+
}
|
|
13
|
+
export function patternCtor(p) {
|
|
14
|
+
return p.kind === "ctor" ? p.ctor : null;
|
|
15
|
+
}
|
|
16
|
+
/** Does the pattern bind `name`? */
|
|
17
|
+
export function patternBinds(p, name) {
|
|
18
|
+
return patternBinders(p).includes(name);
|
|
19
|
+
}
|
|
7
20
|
export function anyExpr(e, pred) {
|
|
8
21
|
if (pred(e))
|
|
9
22
|
return true;
|
|
@@ -75,8 +88,32 @@ export function usesName(e, name) {
|
|
|
75
88
|
export function usesNameInStmts(stmts, name) {
|
|
76
89
|
return anyExprInStmts(stmts, _refsName(name));
|
|
77
90
|
}
|
|
78
|
-
/**
|
|
79
|
-
*
|
|
91
|
+
/** Names a statement tree *binds, targets, or introduces* — `let`/`let-bind`/
|
|
92
|
+
* `ghostLet` names, `assign`/`bind`/`ghostAssign` targets, `for-in` indices,
|
|
93
|
+
* and `match`-arm pattern binders — recursing through nested blocks. Distinct
|
|
94
|
+
* from `usesNameInStmts` (expression references only): an unread or assign-only
|
|
95
|
+
* local still duplicate-declares against a method's out-parameter in Dafny. */
|
|
96
|
+
export function bindsNameInStmts(stmts, name) {
|
|
97
|
+
return stmts.some(s => {
|
|
98
|
+
switch (s.kind) {
|
|
99
|
+
case "let":
|
|
100
|
+
case "let-bind":
|
|
101
|
+
case "ghostLet": return s.name === name;
|
|
102
|
+
case "assign":
|
|
103
|
+
case "bind":
|
|
104
|
+
case "ghostAssign": return s.target === name;
|
|
105
|
+
case "forin": return s.idx === name || bindsNameInStmts(s.body, name);
|
|
106
|
+
case "if": return bindsNameInStmts(s.then, name) || bindsNameInStmts(s.else, name);
|
|
107
|
+
case "while": return bindsNameInStmts(s.body, name);
|
|
108
|
+
case "match": return s.arms.some(a => patternBinds(a.pattern, name) || bindsNameInStmts(a.body, name));
|
|
109
|
+
default: return false;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Every occurrence of `name` a method's out-parameter binder must dodge —
|
|
114
|
+
* referenced in a spec (requires/ensures) or body, *or* bound/targeted anywhere
|
|
115
|
+
* in the body (an unread local still duplicate-declares). Both emitters share it. */
|
|
80
116
|
export function usesNameInDecl(requires, ensures, body, name) {
|
|
81
|
-
return requires.some(e => usesName(e, name)) || ensures.some(e => usesName(e, name))
|
|
117
|
+
return requires.some(e => usesName(e, name)) || ensures.some(e => usesName(e, name))
|
|
118
|
+
|| usesNameInStmts(body, name) || bindsNameInStmts(body, name);
|
|
82
119
|
}
|
package/tools/dist/lean-emit.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lean emitter — IR → Lean text.
|
|
3
|
-
*
|
|
2
|
+
* Lean emitter — IR → Lean text. Beyond serialization it makes type-driven
|
|
3
|
+
* decisions: Bool-vs-Prop connectives, dropping Repr/DecidableEq for
|
|
4
|
+
* opaque-tainted types, discriminator/destructor lowering, method dispatch,
|
|
5
|
+
* and support-import selection.
|
|
4
6
|
*/
|
|
5
|
-
import { anyExpr, usesNameInDecl } from "./ir.js";
|
|
7
|
+
import { anyExpr, usesNameInDecl, patternBinders } from "./ir.js";
|
|
6
8
|
import { freshName } from "./names.js";
|
|
7
9
|
// ── Ty → Lean type string ──────────────────────────────────
|
|
8
10
|
function tyToLean(ty) {
|
|
@@ -178,6 +180,10 @@ let _unknownEmitted = false; // across files in one run — the def file imports
|
|
|
178
180
|
// built only from decidable atoms (comparisons, Bool-returning calls) coerces fine
|
|
179
181
|
// and stays in the more proof-friendly Prop form.
|
|
180
182
|
let _boolCtx = false;
|
|
183
|
+
/** Render a match pattern to Lean syntax: `_`, `.none`, `.some x`, `.syn seq`. */
|
|
184
|
+
function renderLeanPattern(p) {
|
|
185
|
+
return p.kind === "wild" ? "_" : "." + [p.ctor, ...p.binders].join(" ");
|
|
186
|
+
}
|
|
181
187
|
// A Bool-valued atom that does NOT coerce to Prop: an inlined union discriminator
|
|
182
188
|
// (lowered to a match-bool `match x with | .C .. => true | _ => false`) or a raw
|
|
183
189
|
// `match` used as a Bool — neither has a `Decidable` instance Lean can synthesize
|
|
@@ -465,7 +471,7 @@ function emitExpr(e, parentPrec) {
|
|
|
465
471
|
// Always parenthesize inline matches — Lean parses alternatives greedily,
|
|
466
472
|
// so any token after an arm body (`→`, another match's `|`, etc.) would
|
|
467
473
|
// bleed into the last `.none` case without explicit bracketing.
|
|
468
|
-
const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
|
|
474
|
+
const arms = e.arms.map(a => `| ${renderLeanPattern(a.pattern)} => ${emitExpr(a.body)}`);
|
|
469
475
|
const scrut = typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee);
|
|
470
476
|
return `(match ${scrut} with ${arms.join(" ")})`;
|
|
471
477
|
}
|
|
@@ -557,10 +563,10 @@ function emitStmt(s, indent) {
|
|
|
557
563
|
const scrut = typeof s.scrutinee === "string" ? s.scrutinee : emitExpr(s.scrutinee);
|
|
558
564
|
// Option match (.some/.none) → emit as if/let for WPGen.if compatibility
|
|
559
565
|
if (s.arms.length === 2) {
|
|
560
|
-
const someArm = s.arms.find(a => a.pattern.
|
|
561
|
-
const noneArm = s.arms.find(a => a.pattern === ".none");
|
|
566
|
+
const someArm = s.arms.find(a => a.pattern.kind === "ctor" && a.pattern.ctor === "some");
|
|
567
|
+
const noneArm = s.arms.find(a => a.pattern.kind === "ctor" && a.pattern.ctor === "none");
|
|
562
568
|
if (someArm && noneArm) {
|
|
563
|
-
const boundVar = someArm.pattern
|
|
569
|
+
const boundVar = patternBinders(someArm.pattern)[0]; // ".some x" ⇒ "x"
|
|
564
570
|
const hName = `h_${scrut.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
565
571
|
const lines = [
|
|
566
572
|
`${pad}if ${hName} : (${scrut}).isSome = true then`,
|
|
@@ -585,7 +591,7 @@ function emitStmt(s, indent) {
|
|
|
585
591
|
// General match
|
|
586
592
|
const lines = [`${pad}match ${scrut} with`];
|
|
587
593
|
for (const arm of s.arms) {
|
|
588
|
-
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
594
|
+
lines.push(`${pad}| ${renderLeanPattern(arm.pattern)} =>`);
|
|
589
595
|
if (arm.body.length === 0) {
|
|
590
596
|
lines.push(`${pad} pure ()`);
|
|
591
597
|
}
|
|
@@ -744,7 +750,7 @@ function emitPureExpr(e, indent) {
|
|
|
744
750
|
case "match": {
|
|
745
751
|
const lines = [`${pad}match ${typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee)} with`];
|
|
746
752
|
for (const arm of e.arms) {
|
|
747
|
-
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
753
|
+
lines.push(`${pad}| ${renderLeanPattern(arm.pattern)} =>`);
|
|
748
754
|
lines.push(emitPureExpr(arm.body, indent + 1));
|
|
749
755
|
}
|
|
750
756
|
return lines.join("\n");
|
|
@@ -756,6 +762,20 @@ function emitPureExpr(e, indent) {
|
|
|
756
762
|
}
|
|
757
763
|
}
|
|
758
764
|
// ── File emission ────────────────────────────────────────────
|
|
765
|
+
/** Reset per-module emitter state. Call once per module before the types file
|
|
766
|
+
* (not between types and def — the def file reads the types file's registries). */
|
|
767
|
+
export function resetLeanModule() {
|
|
768
|
+
_resultName = "res";
|
|
769
|
+
_unionCtors.clear();
|
|
770
|
+
_opaqueNames.clear();
|
|
771
|
+
_opaqueNames.add("Unknown");
|
|
772
|
+
_typeRefs.clear();
|
|
773
|
+
_taintedTypes.clear();
|
|
774
|
+
_needsJSString = false;
|
|
775
|
+
_needsUnknown = false;
|
|
776
|
+
_unknownEmitted = false;
|
|
777
|
+
_boolCtx = false;
|
|
778
|
+
}
|
|
759
779
|
export function emitLeanFile(file) {
|
|
760
780
|
_needsJSString = false;
|
|
761
781
|
_needsUnknown = false;
|
package/tools/dist/lsc.js
CHANGED
|
@@ -15,7 +15,7 @@ import { narrowModule } from "./narrow.js";
|
|
|
15
15
|
import { autoHavocModule } from "./autohavoc.js";
|
|
16
16
|
import { transformModuleLean, transformModuleDafny } from "./transform.js";
|
|
17
17
|
import { peepholeModule } from "./peephole.js";
|
|
18
|
-
import { emitLeanFile } from "./lean-emit.js";
|
|
18
|
+
import { emitLeanFile, resetLeanModule } from "./lean-emit.js";
|
|
19
19
|
import { emitDafnyFile } from "./dafny-emit.js";
|
|
20
20
|
import { dafnyGen, dafnyCheckDiff, dafnyVerify, dafnyRegen } from "./dafny-commands.js";
|
|
21
21
|
import { leanGen, leanCheck } from "./lean-commands.js";
|
|
@@ -261,6 +261,7 @@ function runFile(cmd, filePath, backend, timeLimit, extraFlags) {
|
|
|
261
261
|
if (typesFile)
|
|
262
262
|
typesFile = peepholeModule(typesFile, "lean");
|
|
263
263
|
defFile = peepholeModule(defFile, "lean");
|
|
264
|
+
resetLeanModule(); // clear per-module emitter state so batch mode doesn't leak into this module
|
|
264
265
|
const typesPath = typesFile ? path.join(dir, `${leanBase}.types.lean`) : null;
|
|
265
266
|
const typesText = typesFile ? emitLeanFile(typesFile) : null;
|
|
266
267
|
const defPath = path.join(dir, `${leanBase}.def.lean`);
|
package/tools/dist/names.js
CHANGED
|
@@ -34,6 +34,11 @@ export function setUserNames(names) {
|
|
|
34
34
|
export function isUserName(name) {
|
|
35
35
|
return _userNames.has(name);
|
|
36
36
|
}
|
|
37
|
+
/** The raw user identifiers, for a backend that needs to allocate its own
|
|
38
|
+
* emitted names against them (e.g. Dafny escaping — see dafny-emit). */
|
|
39
|
+
export function userNames() {
|
|
40
|
+
return [..._userNames];
|
|
41
|
+
}
|
|
37
42
|
/** A toolchain-internal name: `base` verbatim, primed on collision. The one
|
|
38
43
|
* place the priming rule lives. `taken` says what counts as a collision —
|
|
39
44
|
* by default a user-written name anywhere in the module; callers that know
|
package/tools/dist/narrow.js
CHANGED
|
@@ -417,6 +417,26 @@ function ruleImplOptional(e) {
|
|
|
417
417
|
ty: { kind: "bool" },
|
|
418
418
|
};
|
|
419
419
|
}
|
|
420
|
+
/** Apply an optional chain's steps (field / index / call) to a base expr —
|
|
421
|
+
* shared by `ruleOptChain` (base = binder) and `ruleOptChainIndex` (base = arr[i]). */
|
|
422
|
+
function applyChain(body, chain) {
|
|
423
|
+
for (const step of chain) {
|
|
424
|
+
if (step.kind === "field")
|
|
425
|
+
body = { kind: "field", obj: body, field: step.name, ty: step.ty };
|
|
426
|
+
else if (step.kind === "index")
|
|
427
|
+
body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
|
|
428
|
+
else
|
|
429
|
+
body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
|
|
430
|
+
}
|
|
431
|
+
return body;
|
|
432
|
+
}
|
|
433
|
+
/** `0 <= idx && idx < arr.length` — the in-bounds guard for an array index. */
|
|
434
|
+
function arrayBoundsCond(arr, idx) {
|
|
435
|
+
const len = { kind: "field", obj: arr, field: "length", ty: { kind: "int" } };
|
|
436
|
+
const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
|
|
437
|
+
const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
|
|
438
|
+
return { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
|
|
439
|
+
}
|
|
420
440
|
/** Rule (expression): `left ?? right` — nullish coalescing.
|
|
421
441
|
* → `someMatch left { Some(_v) => _v, None => right }`.
|
|
422
442
|
* Single-evaluation: scrutinee may be any expression. */
|
|
@@ -448,11 +468,7 @@ function ruleNullishIndex(e) {
|
|
|
448
468
|
return null;
|
|
449
469
|
if (e.left.obj.ty.kind !== "array")
|
|
450
470
|
return null;
|
|
451
|
-
const
|
|
452
|
-
const len = { kind: "field", obj: e.left.obj, field: "length", ty: { kind: "int" } };
|
|
453
|
-
const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
|
|
454
|
-
const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
|
|
455
|
-
const cond = { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
|
|
471
|
+
const cond = arrayBoundsCond(e.left.obj, e.left.idx);
|
|
456
472
|
return { kind: "conditional", cond, then: e.left, else: e.right, ty: e.ty };
|
|
457
473
|
}
|
|
458
474
|
/** Rule (expression): `arr[i]?.<chain>` — optional chaining on an array index,
|
|
@@ -470,23 +486,8 @@ function ruleOptChainIndex(e) {
|
|
|
470
486
|
return null;
|
|
471
487
|
if (e.obj.obj.ty.kind !== "array")
|
|
472
488
|
return null;
|
|
473
|
-
const
|
|
474
|
-
const
|
|
475
|
-
const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
|
|
476
|
-
const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
|
|
477
|
-
const cond = { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
|
|
478
|
-
let body = e.obj; // arr[i] — in bounds under `cond`
|
|
479
|
-
for (const step of e.chain) {
|
|
480
|
-
if (step.kind === "field") {
|
|
481
|
-
body = { kind: "field", obj: body, field: step.name, ty: step.ty };
|
|
482
|
-
}
|
|
483
|
-
else if (step.kind === "index") {
|
|
484
|
-
body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
|
|
485
|
-
}
|
|
486
|
-
else {
|
|
487
|
-
body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
|
|
488
|
-
}
|
|
489
|
-
}
|
|
489
|
+
const cond = arrayBoundsCond(e.obj.obj, e.obj.idx);
|
|
490
|
+
const body = applyChain(e.obj, e.chain); // arr[i] — in bounds under `cond`
|
|
490
491
|
const undef = { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
491
492
|
return { kind: "conditional", cond, then: body, else: undef, ty: e.ty };
|
|
492
493
|
}
|
|
@@ -501,18 +502,7 @@ function ruleOptChain(e) {
|
|
|
501
502
|
return null;
|
|
502
503
|
const innerTy = e.obj.ty.inner;
|
|
503
504
|
const binder = freshName(`_oc${_ocCounter++}_val`);
|
|
504
|
-
|
|
505
|
-
for (const step of e.chain) {
|
|
506
|
-
if (step.kind === "field") {
|
|
507
|
-
body = { kind: "field", obj: body, field: step.name, ty: step.ty };
|
|
508
|
-
}
|
|
509
|
-
else if (step.kind === "index") {
|
|
510
|
-
body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
|
|
514
|
-
}
|
|
515
|
-
}
|
|
505
|
+
const body = applyChain({ kind: "var", name: binder, ty: innerTy }, e.chain);
|
|
516
506
|
const noneBody = { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
517
507
|
return {
|
|
518
508
|
kind: "someMatch",
|
|
@@ -604,31 +594,39 @@ function ruleConditionalOptionalTruthy(e) {
|
|
|
604
594
|
someBody: e.then, noneBody: e.else, ty: e.ty,
|
|
605
595
|
};
|
|
606
596
|
}
|
|
607
|
-
/**
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
|
|
597
|
+
/** Find the leftmost `parse`-matching conjunct anywhere in an `&&` chain,
|
|
598
|
+
* returning it plus the remaining conjunction. Conjunct order doesn't carry
|
|
599
|
+
* semantic weight, so either side is fine. Shared by the optional and
|
|
600
|
+
* Array.isArray chain extractors — they differ only in `parse`.
|
|
601
|
+
* `(x !== undefined && b) && c` → { check, restCond: b && c }. */
|
|
602
|
+
function extractLeftmostCheck(cond, parse) {
|
|
612
603
|
if (cond.kind !== "binop" || cond.op !== "&&")
|
|
613
604
|
return null;
|
|
614
|
-
const
|
|
615
|
-
if (
|
|
616
|
-
return { check:
|
|
617
|
-
const
|
|
618
|
-
if (
|
|
619
|
-
return { check:
|
|
605
|
+
const left = parse(cond.left);
|
|
606
|
+
if (left)
|
|
607
|
+
return { check: left, restCond: cond.right };
|
|
608
|
+
const right = parse(cond.right);
|
|
609
|
+
if (right)
|
|
610
|
+
return { check: right, restCond: cond.left };
|
|
620
611
|
if (cond.left.kind === "binop" && cond.left.op === "&&") {
|
|
621
|
-
const inner =
|
|
612
|
+
const inner = extractLeftmostCheck(cond.left, parse);
|
|
622
613
|
if (inner)
|
|
623
614
|
return { check: inner.check, restCond: { ...cond, left: inner.restCond } };
|
|
624
615
|
}
|
|
625
616
|
if (cond.right.kind === "binop" && cond.right.op === "&&") {
|
|
626
|
-
const inner =
|
|
617
|
+
const inner = extractLeftmostCheck(cond.right, parse);
|
|
627
618
|
if (inner)
|
|
628
619
|
return { check: inner.check, restCond: { ...cond, right: inner.restCond } };
|
|
629
620
|
}
|
|
630
621
|
return null;
|
|
631
622
|
}
|
|
623
|
+
/** `&&`-chain extractor for a positive optional check. */
|
|
624
|
+
function extractLeftmostOptionalCheck(cond) {
|
|
625
|
+
return extractLeftmostCheck(cond, e => {
|
|
626
|
+
const c = parseSimpleOptionalCheck(e);
|
|
627
|
+
return c && !c.negated ? c : null;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
632
630
|
/** Rule: `if (x !== undefined && rest) then` (no else) where x is a pure
|
|
633
631
|
* access path.
|
|
634
632
|
* → `someMatch x { Some(_x_val) => if rest then then; , None => {} }`.
|
|
@@ -745,31 +743,11 @@ function isNarrowablePath(e) {
|
|
|
745
743
|
return isNarrowablePath(e.obj);
|
|
746
744
|
return false;
|
|
747
745
|
}
|
|
748
|
-
/**
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
* `!Array.isArray(...)` would narrow to the wrong variant for then-body
|
|
752
|
-
* consumers, so we leave those to the existing untouched-conditional path). */
|
|
746
|
+
/** `&&`-chain extractor for `Array.isArray(path)` (positive form only — a negated
|
|
747
|
+
* `!Array.isArray(...)` would narrow to the wrong variant for then-body consumers,
|
|
748
|
+
* so those are left to the untouched-conditional path). */
|
|
753
749
|
function extractLeftmostArrayIsArrayCheck(cond) {
|
|
754
|
-
|
|
755
|
-
return null;
|
|
756
|
-
const leftCheck = parseArrayIsArrayCall(cond.left);
|
|
757
|
-
if (leftCheck)
|
|
758
|
-
return { check: leftCheck, restCond: cond.right };
|
|
759
|
-
const rightCheck = parseArrayIsArrayCall(cond.right);
|
|
760
|
-
if (rightCheck)
|
|
761
|
-
return { check: rightCheck, restCond: cond.left };
|
|
762
|
-
if (cond.left.kind === "binop" && cond.left.op === "&&") {
|
|
763
|
-
const inner = extractLeftmostArrayIsArrayCheck(cond.left);
|
|
764
|
-
if (inner)
|
|
765
|
-
return { check: inner.check, restCond: { ...cond, left: inner.restCond } };
|
|
766
|
-
}
|
|
767
|
-
if (cond.right.kind === "binop" && cond.right.op === "&&") {
|
|
768
|
-
const inner = extractLeftmostArrayIsArrayCheck(cond.right);
|
|
769
|
-
if (inner)
|
|
770
|
-
return { check: inner.check, restCond: { ...cond, right: inner.restCond } };
|
|
771
|
-
}
|
|
772
|
-
return null;
|
|
750
|
+
return extractLeftmostCheck(cond, parseArrayIsArrayCall);
|
|
773
751
|
}
|
|
774
752
|
/** Detect `x.kind === "variant"`, `'key' in x`, or `Array.isArray(x)` (synth
|
|
775
753
|
* array-union) as a positive discriminant check. Returns the scrutinee var
|
package/tools/dist/peephole.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { patternCtor, patternBinders } from "./ir.js";
|
|
1
2
|
// ── Generic walkers (same shape as transform.ts) ─────────────
|
|
2
3
|
function mapExpr(e, f) {
|
|
3
4
|
const hit = f(e);
|
|
@@ -44,21 +45,19 @@ function isMapGet(e) {
|
|
|
44
45
|
return null;
|
|
45
46
|
return { obj: e.obj, key: e.args[0], objTy: e.objTy };
|
|
46
47
|
}
|
|
47
|
-
/**
|
|
48
|
-
function parseSomeBinder(
|
|
49
|
-
if (
|
|
48
|
+
/** Binder of a Some arm — its name, or null for `.some _` / a non-`some` pattern. */
|
|
49
|
+
function parseSomeBinder(p) {
|
|
50
|
+
if (patternCtor(p) !== "some")
|
|
50
51
|
return null;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
return null;
|
|
54
|
-
return rest.split(/\s+/)[0];
|
|
52
|
+
const b = patternBinders(p)[0];
|
|
53
|
+
return b === undefined || b === "_" ? null : b;
|
|
55
54
|
}
|
|
56
55
|
/** Identify a Some/None match's arms. */
|
|
57
56
|
function getSomeNoneArms(arms) {
|
|
58
57
|
if (arms.length !== 2)
|
|
59
58
|
return null;
|
|
60
|
-
const someArm = arms.find(a => a.pattern
|
|
61
|
-
const noneArm = arms.find(a => a.pattern === "
|
|
59
|
+
const someArm = arms.find(a => patternCtor(a.pattern) === "some");
|
|
60
|
+
const noneArm = arms.find(a => patternCtor(a.pattern) === "none");
|
|
62
61
|
if (!someArm || !noneArm)
|
|
63
62
|
return null;
|
|
64
63
|
return { someArm, noneArm, binder: parseSomeBinder(someArm.pattern) };
|
package/tools/dist/transform.js
CHANGED
|
@@ -4,7 +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
|
+
import { anyExprInStmts, pWild, pCtor, patternBinders, patternBinds, patternCtor } from "./ir.js";
|
|
8
8
|
import { parseTsType } from "./types.js";
|
|
9
9
|
import { freshName } from "./names.js";
|
|
10
10
|
// ── Generic IR walkers ──────────────────────────────────────
|
|
@@ -74,11 +74,6 @@ function mapStmt(s, f) {
|
|
|
74
74
|
case "assert": return { ...s, expr: r(s.expr) };
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
/** Binders a match-arm pattern string (`.Ctor a b`, `_x_val`, `_`) introduces. */
|
|
78
|
-
function patternBinders(pattern) {
|
|
79
|
-
const toks = pattern.match(/[A-Za-z_][A-Za-z0-9_']*/g) ?? [];
|
|
80
|
-
return pattern.trimStart().startsWith(".") ? toks.slice(1) : toks;
|
|
81
|
-
}
|
|
82
77
|
/** Rename free occurrences of `from` to `to`, stopping at every construct that
|
|
83
78
|
* rebinds `from` — lambda params, `let`/`let-bind`/`ghostLet` (shadows the
|
|
84
79
|
* rest of the block), `match` arm patterns, `forall`/`exists`, and `for-in`
|
|
@@ -98,7 +93,7 @@ export function renameFreeVar(e, from, to) {
|
|
|
98
93
|
if (x.kind === "match") {
|
|
99
94
|
const scr = typeof x.scrutinee === "string"
|
|
100
95
|
? (x.scrutinee === from ? to : x.scrutinee) : mapExpr(x.scrutinee, f);
|
|
101
|
-
return { ...x, scrutinee: scr, arms: x.arms.map(a =>
|
|
96
|
+
return { ...x, scrutinee: scr, arms: x.arms.map(a => patternBinds(a.pattern, from) ? a : { ...a, body: mapExpr(a.body, f) }) };
|
|
102
97
|
}
|
|
103
98
|
if (x.kind === "lambda") {
|
|
104
99
|
if (x.params.some(p => p.name === from))
|
|
@@ -203,15 +198,19 @@ function matchBinder(fieldName, prefix) {
|
|
|
203
198
|
}
|
|
204
199
|
/** Build a match arm pattern like `.VariantName _v_field1 _v_field2` from variant info. */
|
|
205
200
|
function buildMatchPattern(variantName, fields, scopePrefix) {
|
|
206
|
-
|
|
207
|
-
return `.${variantName}`;
|
|
208
|
-
return `.${variantName} ${fields.map(f => matchBinder(f.name, scopePrefix)).join(" ")}`;
|
|
201
|
+
return pCtor(variantName, ...fields.map(f => matchBinder(f.name, scopePrefix)));
|
|
209
202
|
}
|
|
210
203
|
const _forofCounters = new Map();
|
|
211
204
|
function isNat(ty) { return ty.kind === "nat"; }
|
|
212
205
|
function isIntegral(ty) { return ty.kind === "int" || ty.kind === "nat"; }
|
|
213
206
|
function isArray(ty) { return ty.kind === "array"; }
|
|
214
207
|
function isUser(ty) { return ty.kind === "user"; }
|
|
208
|
+
function isRecordType(ty) {
|
|
209
|
+
if (ty.kind !== "user")
|
|
210
|
+
return false;
|
|
211
|
+
const base = ty.name.includes("<") ? ty.name.slice(0, ty.name.indexOf("<")) : ty.name;
|
|
212
|
+
return _typeDecls.find(d => d.name === base)?.kind === "record";
|
|
213
|
+
}
|
|
215
214
|
/** Truthiness test for a *lowered* value of source type `ty`, used by `||`
|
|
216
215
|
* falsiness lowering. Mirrors narrow.ts's `canBeFalsy`: only int/nat/string/bool
|
|
217
216
|
* values can be falsy in JS (`0`, `""`, `false`); every other value (array, user
|
|
@@ -432,8 +431,8 @@ function lowerExpr(e, binds) {
|
|
|
432
431
|
return {
|
|
433
432
|
kind: "match", scrutinee: lowerExpr(e.expr, binds),
|
|
434
433
|
arms: [
|
|
435
|
-
{ pattern:
|
|
436
|
-
{ pattern: "
|
|
434
|
+
{ pattern: pCtor("some", bound), body: truthy ? { kind: "unop", op: "¬", expr: truthy } : { kind: "bool", value: false } },
|
|
435
|
+
{ pattern: pCtor("none"), body: { kind: "bool", value: true } },
|
|
437
436
|
],
|
|
438
437
|
};
|
|
439
438
|
}
|
|
@@ -485,8 +484,8 @@ function lowerExpr(e, binds) {
|
|
|
485
484
|
return {
|
|
486
485
|
kind: "match", scrutinee: optExpr,
|
|
487
486
|
arms: [
|
|
488
|
-
{ pattern: "
|
|
489
|
-
{ pattern: "
|
|
487
|
+
{ pattern: pCtor("some", "_"), body: { kind: "bool", value: !isNone } },
|
|
488
|
+
{ pattern: pCtor("none"), body: { kind: "bool", value: isNone } },
|
|
490
489
|
],
|
|
491
490
|
};
|
|
492
491
|
}
|
|
@@ -503,8 +502,8 @@ function lowerExpr(e, binds) {
|
|
|
503
502
|
return {
|
|
504
503
|
kind: "match", scrutinee: optExpr,
|
|
505
504
|
arms: [
|
|
506
|
-
{ pattern:
|
|
507
|
-
{ pattern: "
|
|
505
|
+
{ pattern: pCtor("some", bound), body: { kind: "binop", op: cmpOp, left: { kind: "var", name: bound }, right: valExpr } },
|
|
506
|
+
{ pattern: pCtor("none"), body: { kind: "bool", value: noneVal } },
|
|
508
507
|
],
|
|
509
508
|
};
|
|
510
509
|
}
|
|
@@ -522,12 +521,12 @@ function lowerExpr(e, binds) {
|
|
|
522
521
|
return {
|
|
523
522
|
kind: "match", scrutinee: optExpr,
|
|
524
523
|
arms: [
|
|
525
|
-
{ pattern:
|
|
524
|
+
{ pattern: pCtor("some", bound), body: {
|
|
526
525
|
kind: "if", cond: truthy,
|
|
527
526
|
then: { kind: "app", fn: "Some", args: [{ kind: "var", name: bound }] },
|
|
528
527
|
else: { kind: "var", name: "undefined" }
|
|
529
528
|
} },
|
|
530
|
-
{ pattern: "
|
|
529
|
+
{ pattern: pCtor("none"), body: { kind: "var", name: "undefined" } },
|
|
531
530
|
],
|
|
532
531
|
};
|
|
533
532
|
}
|
|
@@ -546,8 +545,8 @@ function lowerExpr(e, binds) {
|
|
|
546
545
|
return {
|
|
547
546
|
kind: "match", scrutinee: optExpr,
|
|
548
547
|
arms: [
|
|
549
|
-
{ pattern:
|
|
550
|
-
{ pattern: "
|
|
548
|
+
{ pattern: pCtor("some", bound), body: someBody },
|
|
549
|
+
{ pattern: pCtor("none"), body: defaultExpr },
|
|
551
550
|
],
|
|
552
551
|
};
|
|
553
552
|
}
|
|
@@ -692,10 +691,10 @@ function lowerExpr(e, binds) {
|
|
|
692
691
|
const baseName = e.obj.ty.name.includes("<") ? e.obj.ty.name.slice(0, e.obj.ty.name.indexOf("<")) : e.obj.ty.name;
|
|
693
692
|
const decl = _typeDecls.find(d => d.name === baseName && d.kind === "discriminated-union");
|
|
694
693
|
if (decl?.variants?.some(v => v.fields.some(f => f.name === e.field))) {
|
|
695
|
-
return { kind: "field", obj: transformExpr(e.obj), field: e.field, fromUnion: baseName };
|
|
694
|
+
return { kind: "field", obj: transformExpr(e.obj), field: e.field, fromUnion: baseName, datatypeField: true };
|
|
696
695
|
}
|
|
697
696
|
}
|
|
698
|
-
return { kind: "field", obj: transformExpr(e.obj), field: e.field };
|
|
697
|
+
return { kind: "field", obj: transformExpr(e.obj), field: e.field, datatypeField: isRecordType(e.obj.ty) };
|
|
699
698
|
case "index": {
|
|
700
699
|
const idx = transformExpr(e.idx);
|
|
701
700
|
if (e.obj.ty.kind === "map") {
|
|
@@ -997,8 +996,8 @@ function lowerExpr(e, binds) {
|
|
|
997
996
|
return {
|
|
998
997
|
kind: "match", scrutinee,
|
|
999
998
|
arms: [
|
|
1000
|
-
{ pattern:
|
|
1001
|
-
{ pattern: "
|
|
999
|
+
{ pattern: pCtor("some", e.binder), body: someBody },
|
|
1000
|
+
{ pattern: pCtor("none"), body: noneBody },
|
|
1002
1001
|
],
|
|
1003
1002
|
};
|
|
1004
1003
|
}
|
|
@@ -1040,7 +1039,7 @@ function lowerExpr(e, binds) {
|
|
|
1040
1039
|
let body = lowerExpr(e.fallthrough, binds);
|
|
1041
1040
|
if (wrapOpt)
|
|
1042
1041
|
body = wrapOptionalBranch(body, e.fallthrough);
|
|
1043
|
-
arms.push({ pattern:
|
|
1042
|
+
arms.push({ pattern: pWild(), body });
|
|
1044
1043
|
}
|
|
1045
1044
|
return { kind: "match", scrutinee: varName ?? scrutinee, arms };
|
|
1046
1045
|
}
|
|
@@ -1082,7 +1081,7 @@ function ensuresToMatch(e, typeDecls) {
|
|
|
1082
1081
|
const pattern = buildMatchPattern(variantName, fields, obj.name);
|
|
1083
1082
|
let rhs = transformExpr(e.right);
|
|
1084
1083
|
rhs = replaceFieldAccess(rhs, obj.name, fields);
|
|
1085
|
-
return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern:
|
|
1084
|
+
return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: pWild(), body: { kind: "bool", value: true } }] };
|
|
1086
1085
|
}
|
|
1087
1086
|
function replaceFieldAccess(e, varName, fields) {
|
|
1088
1087
|
return mapExpr(e, x => {
|
|
@@ -1269,8 +1268,8 @@ function matchToIfChains(stmts) {
|
|
|
1269
1268
|
if (s.kind !== "match")
|
|
1270
1269
|
return [s];
|
|
1271
1270
|
const arms = s.arms.map(a => ({ ...a, body: matchToIfChains(a.body) }));
|
|
1272
|
-
const ctorArms = arms.filter(a => a.pattern.
|
|
1273
|
-
const firstCtor = ctorArms[0]
|
|
1271
|
+
const ctorArms = arms.filter(a => a.pattern.kind !== "wild");
|
|
1272
|
+
const firstCtor = ctorArms[0] ? patternCtor(ctorArms[0].pattern) : undefined;
|
|
1274
1273
|
const decl = firstCtor
|
|
1275
1274
|
? _typeDecls.find(d => (d.kind === "discriminated-union" || d.kind === "string-union") &&
|
|
1276
1275
|
((d.variants?.some(v => v.name === firstCtor)) || (d.values?.includes(firstCtor))))
|
|
@@ -1278,15 +1277,14 @@ function matchToIfChains(stmts) {
|
|
|
1278
1277
|
if (!decl)
|
|
1279
1278
|
return [{ ...s, arms }]; // not a user union (e.g. Option) — leave as match
|
|
1280
1279
|
const scrutExpr = typeof s.scrutinee === "string" ? { kind: "var", name: s.scrutinee } : s.scrutinee;
|
|
1281
|
-
const defaultArm = arms.find(a => a.pattern.
|
|
1280
|
+
const defaultArm = arms.find(a => a.pattern.kind === "wild");
|
|
1282
1281
|
let elseBranch = defaultArm ? defaultArm.body : [];
|
|
1283
1282
|
for (let k = ctorArms.length - 1; k >= 0; k--) {
|
|
1284
1283
|
const armBody = ctorArms[k].body;
|
|
1285
1284
|
if (armBody.length === 0)
|
|
1286
1285
|
continue; // empty arm (no-op) — let it fall through to `else`
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
const binders = toks.slice(1);
|
|
1286
|
+
const ctor = patternCtor(ctorArms[k].pattern) ?? "";
|
|
1287
|
+
const binders = patternBinders(ctorArms[k].pattern);
|
|
1290
1288
|
const variant = decl.variants?.find(v => v.name === ctor);
|
|
1291
1289
|
// Discriminator condition. A nullary discriminated-union constructor would
|
|
1292
1290
|
// need `DecidableEq` for `x = .Ctor` (which such unions don't derive), so
|
|
@@ -1294,8 +1292,8 @@ function matchToIfChains(stmts) {
|
|
|
1294
1292
|
// string-unions derive DecidableEq, so `=` is fine there.
|
|
1295
1293
|
const cond = decl.kind === "discriminated-union" && binders.length === 0
|
|
1296
1294
|
? { kind: "match", scrutinee: scrutExpr, arms: [
|
|
1297
|
-
{ pattern:
|
|
1298
|
-
{ pattern:
|
|
1295
|
+
{ pattern: pCtor(ctor), body: { kind: "bool", value: true } },
|
|
1296
|
+
{ pattern: pWild(), body: { kind: "bool", value: false } }
|
|
1299
1297
|
] }
|
|
1300
1298
|
: { kind: "binop", op: "=", left: scrutExpr, right: { kind: "constructor", name: ctor, type: decl.name } };
|
|
1301
1299
|
// Bind only the constructor-field binders the body actually uses, pinning the
|
|
@@ -1681,8 +1679,8 @@ function transformStmt(s, typeDecls) {
|
|
|
1681
1679
|
return [{
|
|
1682
1680
|
kind: "match", scrutinee,
|
|
1683
1681
|
arms: [
|
|
1684
|
-
{ pattern:
|
|
1685
|
-
{ pattern: "
|
|
1682
|
+
{ pattern: pCtor("some", s.binder), body: someBody },
|
|
1683
|
+
{ pattern: pCtor("none"), body: noneBody },
|
|
1686
1684
|
],
|
|
1687
1685
|
}];
|
|
1688
1686
|
}
|
|
@@ -1766,7 +1764,7 @@ function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
|
|
|
1766
1764
|
arms.push({ pattern, body });
|
|
1767
1765
|
}
|
|
1768
1766
|
else {
|
|
1769
|
-
arms.push({ pattern:
|
|
1767
|
+
arms.push({ pattern: pWild(), body: transformStmts(fallthrough, typeDecls) });
|
|
1770
1768
|
}
|
|
1771
1769
|
}
|
|
1772
1770
|
return { kind: "match", scrutinee: isPath ? transformExpr(scrutinee) : prefix, arms };
|
|
@@ -1822,7 +1820,7 @@ function emitSwitchStmt(s, typeDecls) {
|
|
|
1822
1820
|
? buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformStmts(body, typeDecls))
|
|
1823
1821
|
: buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", s.expr.ty.kind === "user" ? s.expr.ty.name : undefined, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
|
|
1824
1822
|
if (s.defaultBody.length > 0)
|
|
1825
|
-
arms.push({ pattern:
|
|
1823
|
+
arms.push({ pattern: pWild(), body: transformStmts(s.defaultBody, typeDecls) });
|
|
1826
1824
|
return { kind: "match", scrutinee: ef ? ef.scrutinee : (s.expr.kind === "var" ? s.expr.name : "?"), arms };
|
|
1827
1825
|
}
|
|
1828
1826
|
/** Replace obj.field → replacement var in typed IR.
|
|
@@ -1954,8 +1952,8 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1954
1952
|
return {
|
|
1955
1953
|
kind: "match", scrutinee,
|
|
1956
1954
|
arms: [
|
|
1957
|
-
{ pattern:
|
|
1958
|
-
{ pattern: "
|
|
1955
|
+
{ pattern: pCtor("some", s.binder), body: someExpr },
|
|
1956
|
+
{ pattern: pCtor("none"), body: noneExpr },
|
|
1959
1957
|
],
|
|
1960
1958
|
};
|
|
1961
1959
|
}
|
|
@@ -1977,7 +1975,7 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
1977
1975
|
const body = transformPureBody(s.defaultBody, typeDecls);
|
|
1978
1976
|
if (!body)
|
|
1979
1977
|
return null;
|
|
1980
|
-
arms.push({ pattern:
|
|
1978
|
+
arms.push({ pattern: pWild(), body });
|
|
1981
1979
|
}
|
|
1982
1980
|
return { kind: "match", scrutinee: ef.scrutinee, arms };
|
|
1983
1981
|
}
|
|
@@ -2000,7 +1998,7 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
2000
1998
|
const body = transformPureBody(s.defaultBody, typeDecls);
|
|
2001
1999
|
if (!body)
|
|
2002
2000
|
return null;
|
|
2003
|
-
arms.push({ pattern:
|
|
2001
|
+
arms.push({ pattern: pWild(), body });
|
|
2004
2002
|
}
|
|
2005
2003
|
if (s.expr.kind !== "var")
|
|
2006
2004
|
return null;
|
|
@@ -2048,7 +2046,7 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
2048
2046
|
const body = transformPureBody(chain.fallthrough, typeDecls);
|
|
2049
2047
|
if (!body)
|
|
2050
2048
|
return null;
|
|
2051
|
-
arms.push({ pattern:
|
|
2049
|
+
arms.push({ pattern: pWild(), body });
|
|
2052
2050
|
}
|
|
2053
2051
|
}
|
|
2054
2052
|
return { kind: "match", scrutinee: chain.varName, arms };
|