lemmascript 0.5.7 → 0.5.8
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 +1 -1
- package/tools/dist/dafny-emit.js +47 -4
- package/tools/dist/emit.js +253 -0
- package/tools/dist/extract.js +106 -39
- package/tools/dist/narrow.js +56 -1
- package/tools/dist/resolve.js +147 -5
- package/tools/dist/transform.js +3 -2
- package/tools/dist/types.js +14 -0
- package/tools/dist/guard-command.js +0 -238
package/package.json
CHANGED
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -19,7 +19,11 @@ function tyToDafny(ty) {
|
|
|
19
19
|
}
|
|
20
20
|
case "user": return ty.name;
|
|
21
21
|
case "fn": return `(${ty.params.map(tyToDafny).join(", ")}) -> ${tyToDafny(ty.result)}`;
|
|
22
|
-
|
|
22
|
+
// Out-of-subset (`any`/`unknown`); opaque so real ops on it fail loudly
|
|
23
|
+
// rather than silently verify as `int`. Mirrors the Lean backend's `_`.
|
|
24
|
+
case "unknown":
|
|
25
|
+
needPreamble("UnknownType");
|
|
26
|
+
return "Unknown";
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
// ── Dafny keyword escaping ──────────────────────────────────
|
|
@@ -159,8 +163,14 @@ function emitExpr(e) {
|
|
|
159
163
|
}
|
|
160
164
|
if (e.method === "push")
|
|
161
165
|
return `(${obj} + [${args.join(", ")}])`;
|
|
166
|
+
if (e.method === "unshift")
|
|
167
|
+
return `([${args.join(", ")}] + ${obj})`;
|
|
162
168
|
if (e.method === "concat")
|
|
163
169
|
return `(${obj} + [${args.join(", ")}])`;
|
|
170
|
+
if (e.method === "sort") {
|
|
171
|
+
needPreamble("SeqSortBy");
|
|
172
|
+
return `SeqSortBy(${obj}, ${args[0]})`;
|
|
173
|
+
}
|
|
164
174
|
// No-arg slice is a full copy; Dafny seq is an immutable value type, so
|
|
165
175
|
// the copy is just the seq itself (the idiom for "copy then mutate").
|
|
166
176
|
if (e.method === "slice" && args.length === 0)
|
|
@@ -671,7 +681,8 @@ function emitDecl(d) {
|
|
|
671
681
|
return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
|
|
672
682
|
}
|
|
673
683
|
case "structure": {
|
|
674
|
-
|
|
684
|
+
const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
|
|
685
|
+
return `datatype ${d.name}${tp} = ${d.name}(${paramList(d.fields)})`;
|
|
675
686
|
}
|
|
676
687
|
case "type-alias": {
|
|
677
688
|
return `type ${d.name} = ${tyToDafny(d.target)}`;
|
|
@@ -964,6 +975,17 @@ const STRING_SPLIT = `function {:axiom} StringSplit(s: string, d: string): seq<s
|
|
|
964
975
|
ensures |StringSplit(s, d)| >= 1
|
|
965
976
|
ensures |StringSplit(s, d)| <= |s| + 1
|
|
966
977
|
ensures forall k :: 0 <= k < |StringSplit(s, d)| ==> |StringSplit(s, d)[k]| <= |s|`;
|
|
978
|
+
// `xs.sort(cmp)` in TS sorts in place by a comparator (negative ⟺ a before b).
|
|
979
|
+
// Modeled here as an axiom returning a permutation sorted by cmp. The `requires`
|
|
980
|
+
// is the soundness condition — cmp must be a total preorder, otherwise no sorted
|
|
981
|
+
// permutation exists and the axiom would be vacuous. Callers discharge it (e.g.
|
|
982
|
+
// `(a,b) => a.k - b.k` is total + transitive by linear arithmetic).
|
|
983
|
+
const SEQ_SORT_BY = `function {:axiom} SeqSortBy<T(==,!new)>(s: seq<T>, cmp: (T, T) -> int): seq<T>
|
|
984
|
+
requires forall a: T, b: T :: cmp(a, b) <= 0 || cmp(b, a) <= 0
|
|
985
|
+
requires forall a: T, b: T, c: T :: cmp(a, b) <= 0 && cmp(b, c) <= 0 ==> cmp(a, c) <= 0
|
|
986
|
+
ensures multiset(SeqSortBy(s, cmp)) == multiset(s)
|
|
987
|
+
ensures |SeqSortBy(s, cmp)| == |s|
|
|
988
|
+
ensures forall i: int, j: int :: 0 <= i <= j < |SeqSortBy(s, cmp)| ==> cmp(SeqSortBy(s, cmp)[i], SeqSortBy(s, cmp)[j]) <= 0`;
|
|
967
989
|
const STRING_TRIM = `function StringTrimLeft(s: string): string
|
|
968
990
|
ensures |StringTrimLeft(s)| <= |s|
|
|
969
991
|
ensures StringTrimLeft(s) == "" || (|StringTrimLeft(s)| > 0 && StringTrimLeft(s)[0] != ' ')
|
|
@@ -1095,6 +1117,9 @@ const SET_TO_SEQ = `method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
|
|
|
1095
1117
|
/** Preamble code keyed by name. Emitted in this order when needed. */
|
|
1096
1118
|
const PREAMBLE_CODE = [
|
|
1097
1119
|
["OptionType", "datatype Option<T> = None | Some(value: T)"],
|
|
1120
|
+
// Opaque carrier for `unknown`-typed values. `(==)` for compare/map-key/match;
|
|
1121
|
+
// `(0)` (auto-init ⇒ nonempty) so `havoc` (`:= *`) is well-formed.
|
|
1122
|
+
["UnknownType", "type Unknown(==, 0)"],
|
|
1098
1123
|
["SetToSeq", SET_TO_SEQ],
|
|
1099
1124
|
["Pow2", POW2],
|
|
1100
1125
|
["BitAnd", BIT_AND],
|
|
@@ -1114,6 +1139,7 @@ const PREAMBLE_CODE = [
|
|
|
1114
1139
|
["SafeSlice", SAFE_SLICE],
|
|
1115
1140
|
["StringIndexOf", STRING_INDEX_OF],
|
|
1116
1141
|
["StringSplit", STRING_SPLIT],
|
|
1142
|
+
["SeqSortBy", SEQ_SORT_BY],
|
|
1117
1143
|
["StringTrim", STRING_TRIM],
|
|
1118
1144
|
["StringToLower", STRING_TO_LOWER],
|
|
1119
1145
|
["StringToUpper", STRING_TO_UPPER],
|
|
@@ -1202,6 +1228,23 @@ export function emitDafnyFile(file, tsFileName, opts) {
|
|
|
1202
1228
|
// Track successfully emitted pure defs — method wrappers are only
|
|
1203
1229
|
// skipped when the corresponding pure def was actually emitted.
|
|
1204
1230
|
const emittedPureDefs = new Set();
|
|
1231
|
+
// Emit a decl, rolling back any preamble requirements it registered if it
|
|
1232
|
+
// throws. A skipped decl must contribute neither text nor preambles — else a
|
|
1233
|
+
// side-effecting `needPreamble` from a half-emitted decl leaves an unused
|
|
1234
|
+
// preamble (e.g. `type Unknown` from a skipped const whose head is
|
|
1235
|
+
// `unknown`-typed but whose value expr is unsupported).
|
|
1236
|
+
const emitDeclTx = (d) => {
|
|
1237
|
+
const saved = new Set(_neededPreambles);
|
|
1238
|
+
try {
|
|
1239
|
+
return emitDecl(d);
|
|
1240
|
+
}
|
|
1241
|
+
catch (e) {
|
|
1242
|
+
_neededPreambles.clear();
|
|
1243
|
+
for (const k of saved)
|
|
1244
|
+
_neededPreambles.add(k);
|
|
1245
|
+
throw e;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1205
1248
|
// Emit declarations
|
|
1206
1249
|
const declLines = [];
|
|
1207
1250
|
const skipped = [];
|
|
@@ -1214,7 +1257,7 @@ export function emitDafnyFile(file, tsFileName, opts) {
|
|
|
1214
1257
|
for (const inner of decl.decls) {
|
|
1215
1258
|
try {
|
|
1216
1259
|
declLines.push("");
|
|
1217
|
-
declLines.push(
|
|
1260
|
+
declLines.push(emitDeclTx(inner));
|
|
1218
1261
|
if (inner.kind === "def")
|
|
1219
1262
|
emittedPureDefs.add(inner.name);
|
|
1220
1263
|
}
|
|
@@ -1230,7 +1273,7 @@ export function emitDafnyFile(file, tsFileName, opts) {
|
|
|
1230
1273
|
}
|
|
1231
1274
|
try {
|
|
1232
1275
|
declLines.push("");
|
|
1233
|
-
declLines.push(
|
|
1276
|
+
declLines.push(emitDeclTx(decl));
|
|
1234
1277
|
if (decl.kind === "def-by-method")
|
|
1235
1278
|
emittedPureDefs.add(decl.name);
|
|
1236
1279
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lean IR → text. Trivial pretty-printer.
|
|
3
|
+
* No logic, no type decisions — just serialization.
|
|
4
|
+
*/
|
|
5
|
+
// ── Lean keyword escaping ────────────────────────────────────
|
|
6
|
+
const LEAN_KEYWORDS = new Set([
|
|
7
|
+
"def", "theorem", "lemma", "example", "structure", "class", "instance",
|
|
8
|
+
"inductive", "where", "match", "with", "if", "then", "else", "do",
|
|
9
|
+
"let", "mut", "return", "for", "in", "while", "break", "continue",
|
|
10
|
+
"import", "open", "section", "namespace", "end", "set_option",
|
|
11
|
+
"variable", "axiom", "constant", "private", "protected", "noncomputable",
|
|
12
|
+
"partial", "unsafe", "macro", "syntax", "by", "fun", "have", "show",
|
|
13
|
+
"at", "from", "to", "deriving", "extends", "true", "false",
|
|
14
|
+
]);
|
|
15
|
+
function escapeName(name) {
|
|
16
|
+
return LEAN_KEYWORDS.has(name) ? `«${name}»` : name;
|
|
17
|
+
}
|
|
18
|
+
// ── Operator precedence (for parenthesization) ──────────────
|
|
19
|
+
const PREC = {
|
|
20
|
+
"→": 1, "∨": 2, "∧": 3,
|
|
21
|
+
"=": 4, "≠": 4, "≥": 4, "≤": 4, ">": 4, "<": 4,
|
|
22
|
+
"+": 5, "-": 5, "*": 6, "/": 6, "%": 6,
|
|
23
|
+
};
|
|
24
|
+
function prec(op) { return PREC[op] ?? 10; }
|
|
25
|
+
// ── Expression emission ─────────────────────────────────────
|
|
26
|
+
function emitExpr(e, parentPrec) {
|
|
27
|
+
switch (e.kind) {
|
|
28
|
+
case "var": return escapeName(e.name);
|
|
29
|
+
case "num": return `${e.value}`;
|
|
30
|
+
case "bool": return e.value ? "true" : "false";
|
|
31
|
+
case "str": return `"${e.value}"`;
|
|
32
|
+
case "constructor": return `.${e.name}`;
|
|
33
|
+
case "arrayLiteral":
|
|
34
|
+
if (e.elems.length === 0)
|
|
35
|
+
return `#[]`;
|
|
36
|
+
return `#[${e.elems.map(el => emitExpr(el)).join(", ")}]`;
|
|
37
|
+
case "dotCall": {
|
|
38
|
+
const obj = emitExpr(e.obj);
|
|
39
|
+
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "dotCall";
|
|
40
|
+
const receiver = wrap ? `(${obj})` : obj;
|
|
41
|
+
const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
|
|
42
|
+
return args.length > 0 ? `${receiver}.${e.method} ${args.join(" ")}` : `${receiver}.${e.method}`;
|
|
43
|
+
}
|
|
44
|
+
case "lambda": {
|
|
45
|
+
const params = e.params.map(p => p.name).join(" ");
|
|
46
|
+
// Single return statement → expression lambda
|
|
47
|
+
if (e.body.length === 1 && e.body[0].kind === "return") {
|
|
48
|
+
return `(fun ${params} => ${emitExpr(e.body[0].value)})`;
|
|
49
|
+
}
|
|
50
|
+
// Multi-statement → do block
|
|
51
|
+
return `(fun ${params} => do\n${emitStmts(e.body, 2)})`;
|
|
52
|
+
}
|
|
53
|
+
case "unop":
|
|
54
|
+
if (e.op === "¬")
|
|
55
|
+
return `¬(${emitExpr(e.expr)})`;
|
|
56
|
+
if (e.op === "-" && e.expr.kind === "num")
|
|
57
|
+
return `-${e.expr.value}`;
|
|
58
|
+
return `(-${emitExpr(e.expr)})`;
|
|
59
|
+
case "binop": {
|
|
60
|
+
const s = `${emitExpr(e.left, prec(e.op))} ${e.op} ${emitExpr(e.right, prec(e.op))}`;
|
|
61
|
+
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
62
|
+
}
|
|
63
|
+
case "implies": {
|
|
64
|
+
const parts = [...e.premises.map(p => emitExpr(p)), emitExpr(e.conclusion)];
|
|
65
|
+
const s = parts.join(" → ");
|
|
66
|
+
return parentPrec !== undefined ? `(${s})` : s;
|
|
67
|
+
}
|
|
68
|
+
case "app": {
|
|
69
|
+
const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
|
|
70
|
+
return `${e.fn} ${args.join(" ")}`;
|
|
71
|
+
}
|
|
72
|
+
case "field": {
|
|
73
|
+
const obj = emitExpr(e.obj);
|
|
74
|
+
const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bool";
|
|
75
|
+
return wrap ? `(${obj}).${escapeName(e.field)}` : `${obj}.${escapeName(e.field)}`;
|
|
76
|
+
}
|
|
77
|
+
case "toNat": {
|
|
78
|
+
const inner = emitExpr(e.expr);
|
|
79
|
+
const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
|
|
80
|
+
return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
|
|
81
|
+
}
|
|
82
|
+
case "index":
|
|
83
|
+
return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
|
|
84
|
+
case "record": {
|
|
85
|
+
const fields = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
|
|
86
|
+
if (e.spread)
|
|
87
|
+
return `{ ${emitExpr(e.spread)} with ${fields.join(", ")} }`;
|
|
88
|
+
return `{ ${fields.join(", ")} }`;
|
|
89
|
+
}
|
|
90
|
+
case "if":
|
|
91
|
+
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
92
|
+
case "match": {
|
|
93
|
+
const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
|
|
94
|
+
return `match ${e.scrutinee} with ${arms.join(" ")}`;
|
|
95
|
+
}
|
|
96
|
+
case "forall": return `∀ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
|
|
97
|
+
case "exists": return `∃ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
|
|
98
|
+
case "let": return `let ${e.name} := ${emitExpr(e.value)}\n${emitExpr(e.body)}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ── Statement emission ──────────────────────────────────────
|
|
102
|
+
function emitStmts(stmts, indent) {
|
|
103
|
+
const pad = " ".repeat(indent);
|
|
104
|
+
return stmts.map(s => emitStmt(s, indent)).join("\n");
|
|
105
|
+
}
|
|
106
|
+
function emitStmt(s, indent) {
|
|
107
|
+
const pad = " ".repeat(indent);
|
|
108
|
+
switch (s.kind) {
|
|
109
|
+
case "let":
|
|
110
|
+
return s.mutable
|
|
111
|
+
? `${pad}let mut ${escapeName(s.name)} : ${s.type} := ${emitExpr(s.value)}`
|
|
112
|
+
: `${pad}let ${escapeName(s.name)} := ${emitExpr(s.value)}`;
|
|
113
|
+
case "assign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
|
|
114
|
+
case "bind": return `${pad}${escapeName(s.target)} ← ${emitExpr(s.value)}`;
|
|
115
|
+
case "let-bind": return `${pad}let ${s.name} ← ${emitExpr(s.value)}`;
|
|
116
|
+
case "return": return `${pad}return ${emitExpr(s.value)}`;
|
|
117
|
+
case "break": return `${pad}break`;
|
|
118
|
+
case "continue": return `${pad}continue`;
|
|
119
|
+
case "if": {
|
|
120
|
+
let out = `${pad}if ${emitExpr(s.cond)} then\n${emitStmts(s.then, indent + 1)}`;
|
|
121
|
+
if (s.else.length > 0) {
|
|
122
|
+
if (s.else.length === 1 && s.else[0].kind === "if") {
|
|
123
|
+
const ei = s.else[0];
|
|
124
|
+
out += `\n${pad}else if ${emitExpr(ei.cond)} then\n${emitStmts(ei.then, indent + 1)}`;
|
|
125
|
+
if (ei.else.length > 0)
|
|
126
|
+
out += `\n${pad}else\n${emitStmts(ei.else, indent + 1)}`;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
out += `\n${pad}else\n${emitStmts(s.else, indent + 1)}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
case "match": {
|
|
135
|
+
const lines = [`${pad}match ${s.scrutinee} with`];
|
|
136
|
+
for (const arm of s.arms) {
|
|
137
|
+
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
138
|
+
lines.push(emitStmts(arm.body, indent + 1));
|
|
139
|
+
}
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
case "while": {
|
|
143
|
+
const lines = [`${pad}while ${emitExpr(s.cond)}`];
|
|
144
|
+
for (const inv of s.invariants)
|
|
145
|
+
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
146
|
+
if (s.doneWith)
|
|
147
|
+
lines.push(`${pad} done_with ${emitExpr(s.doneWith)}`);
|
|
148
|
+
if (s.decreasing)
|
|
149
|
+
lines.push(`${pad} decreasing ${emitExpr(s.decreasing)}`);
|
|
150
|
+
lines.push(`${pad}do`);
|
|
151
|
+
lines.push(emitStmts(s.body, indent + 1));
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
case "forin": {
|
|
155
|
+
const lines = [`${pad}for ${s.idx} in [:${emitExpr(s.bound)}]`];
|
|
156
|
+
for (const inv of s.invariants)
|
|
157
|
+
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
158
|
+
lines.push(`${pad}do`);
|
|
159
|
+
lines.push(emitStmts(s.body, indent + 1));
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ── Declaration emission ─────────────────────────────────────
|
|
165
|
+
function emitDecl(d) {
|
|
166
|
+
switch (d.kind) {
|
|
167
|
+
case "inductive": {
|
|
168
|
+
const lines = [`inductive ${d.name} where`];
|
|
169
|
+
for (const c of d.constructors) {
|
|
170
|
+
if (c.fields.length === 0) {
|
|
171
|
+
lines.push(` | ${c.name} : ${d.name}`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const params = c.fields.map(f => `(${escapeName(f.name)} : ${f.type})`).join(" ");
|
|
175
|
+
lines.push(` | ${c.name} ${params} : ${d.name}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (d.deriving.length > 0)
|
|
179
|
+
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
}
|
|
182
|
+
case "structure": {
|
|
183
|
+
const lines = [`structure ${d.name} where`];
|
|
184
|
+
for (const f of d.fields)
|
|
185
|
+
lines.push(` ${escapeName(f.name)} : ${f.type}`);
|
|
186
|
+
if (d.deriving.length > 0)
|
|
187
|
+
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
case "def": {
|
|
191
|
+
const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
|
|
192
|
+
return `def ${d.name} ${params} : ${d.returnType} :=\n${emitPureExpr(d.body, 1)}`;
|
|
193
|
+
}
|
|
194
|
+
case "method": {
|
|
195
|
+
const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
|
|
196
|
+
const lines = [`method ${d.name} ${params} return (res : ${d.returnType})`];
|
|
197
|
+
for (const r of d.requires)
|
|
198
|
+
lines.push(` require ${emitExpr(r)}`);
|
|
199
|
+
for (const e of d.ensures)
|
|
200
|
+
lines.push(` ensures ${emitExpr(e)}`);
|
|
201
|
+
lines.push(" do");
|
|
202
|
+
lines.push(emitStmts(d.body, 2));
|
|
203
|
+
return lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
case "namespace": {
|
|
206
|
+
const lines = [`namespace ${d.name}`];
|
|
207
|
+
for (const inner of d.decls)
|
|
208
|
+
lines.push("", emitDecl(inner));
|
|
209
|
+
lines.push("", `end ${d.name}`);
|
|
210
|
+
return lines.join("\n");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** Emit a pure expression with indented if/match blocks. */
|
|
215
|
+
function emitPureExpr(e, indent) {
|
|
216
|
+
const pad = " ".repeat(indent);
|
|
217
|
+
switch (e.kind) {
|
|
218
|
+
case "if":
|
|
219
|
+
return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
|
|
220
|
+
case "match": {
|
|
221
|
+
const lines = [`${pad}match ${e.scrutinee} with`];
|
|
222
|
+
for (const arm of e.arms) {
|
|
223
|
+
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
224
|
+
lines.push(emitPureExpr(arm.body, indent + 1));
|
|
225
|
+
}
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
228
|
+
case "let":
|
|
229
|
+
return `${pad}let ${e.name} := ${emitExpr(e.value)}\n${emitPureExpr(e.body, indent)}`;
|
|
230
|
+
default:
|
|
231
|
+
return `${pad}${emitExpr(e)}`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// ── File emission ────────────────────────────────────────────
|
|
235
|
+
export function emitFile(file) {
|
|
236
|
+
const lines = [];
|
|
237
|
+
if (file.comment) {
|
|
238
|
+
lines.push("/-");
|
|
239
|
+
lines.push(file.comment);
|
|
240
|
+
lines.push("-/");
|
|
241
|
+
}
|
|
242
|
+
for (const imp of file.imports)
|
|
243
|
+
lines.push(`import ${imp}`);
|
|
244
|
+
if (file.options.length > 0)
|
|
245
|
+
lines.push("");
|
|
246
|
+
for (const opt of file.options)
|
|
247
|
+
lines.push(`set_option ${opt.key} ${opt.value}`);
|
|
248
|
+
for (const decl of file.decls) {
|
|
249
|
+
lines.push("");
|
|
250
|
+
lines.push(emitDecl(decl));
|
|
251
|
+
}
|
|
252
|
+
return lines.join("\n") + "\n";
|
|
253
|
+
}
|
package/tools/dist/extract.js
CHANGED
|
@@ -487,9 +487,9 @@ function extractExpr(node) {
|
|
|
487
487
|
}
|
|
488
488
|
// Object literal: { res: true, done: false } or { ...obj, res: true }
|
|
489
489
|
if (Node.isObjectLiteralExpression(node)) {
|
|
490
|
-
// Fold properties in source order: a
|
|
491
|
-
//
|
|
492
|
-
//
|
|
490
|
+
// Fold properties in source order: a spread on top of existing content is a
|
|
491
|
+
// field-wise merge (resolve expands it against the result type), a named field
|
|
492
|
+
// is a record-update on the accumulator, a computed key is a map `.set`.
|
|
493
493
|
let acc = null;
|
|
494
494
|
const update = (name, value) => {
|
|
495
495
|
acc = acc && acc.kind === "record"
|
|
@@ -498,7 +498,8 @@ function extractExpr(node) {
|
|
|
498
498
|
};
|
|
499
499
|
for (const prop of node.getProperties()) {
|
|
500
500
|
if (Node.isSpreadAssignment(prop)) {
|
|
501
|
-
|
|
501
|
+
const s = extractExpr(prop.getExpression());
|
|
502
|
+
acc = acc === null ? s : { kind: "recordMerge", base: acc, override: s };
|
|
502
503
|
}
|
|
503
504
|
else if (Node.isShorthandPropertyAssignment(prop)) {
|
|
504
505
|
const name = prop.getName();
|
|
@@ -985,6 +986,14 @@ function forCounterRename(decl, forStmt) {
|
|
|
985
986
|
const scopeRoot = fnLike ?? forStmt.getSourceFile();
|
|
986
987
|
const isScope = (a) => Node.isBlock(a) || Node.isSourceFile(a) ||
|
|
987
988
|
Node.isForStatement(a) || Node.isForOfStatement(a) || Node.isForInStatement(a);
|
|
989
|
+
// A counting-`for` counter is hoisted out of its `for`, so its real scope is
|
|
990
|
+
// the nearest enclosing block, not the `for` itself.
|
|
991
|
+
const hoistScope = (d) => {
|
|
992
|
+
const owner = d.getParent()?.getParent();
|
|
993
|
+
return owner && Node.isForStatement(owner) && owner.getInitializer() === d.getParent()
|
|
994
|
+
? d.getFirstAncestor(a => Node.isBlock(a) || Node.isSourceFile(a))
|
|
995
|
+
: undefined;
|
|
996
|
+
};
|
|
988
997
|
// Scopes that enclose this loop — its counter, once hoisted, lives in one of
|
|
989
998
|
// these, so a same-named binding scoped here would collide.
|
|
990
999
|
const enclosing = new Set(forStmt.getAncestors());
|
|
@@ -992,6 +1001,11 @@ function forCounterRename(decl, forStmt) {
|
|
|
992
1001
|
const declClash = scopeRoot.getDescendantsOfKind(SyntaxKind.VariableDeclaration).some(d => {
|
|
993
1002
|
if (d === decl || d.getName() !== name)
|
|
994
1003
|
return false;
|
|
1004
|
+
// Two sibling for-counters hoisting into the same block: rename only the
|
|
1005
|
+
// later loop, so the first keeps its name. Any other clash (e.g. a `let`) yields.
|
|
1006
|
+
const counterScope = hoistScope(d);
|
|
1007
|
+
if (counterScope)
|
|
1008
|
+
return enclosing.has(counterScope) && d.getStart() < decl.getStart();
|
|
995
1009
|
const scope = d.getFirstAncestor(isScope);
|
|
996
1010
|
return !!scope && enclosing.has(scope);
|
|
997
1011
|
});
|
|
@@ -1027,6 +1041,7 @@ function renameRawExpr(e, from, to) {
|
|
|
1027
1041
|
case "index": return { ...e, obj: r(e.obj), idx: r(e.idx) };
|
|
1028
1042
|
case "field": return { ...e, obj: r(e.obj) };
|
|
1029
1043
|
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(f => ({ ...f, value: r(f.value) })) };
|
|
1044
|
+
case "recordMerge": return { ...e, base: r(e.base), override: r(e.override) };
|
|
1030
1045
|
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
1031
1046
|
case "conditional": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
|
|
1032
1047
|
case "nullish": return { ...e, left: r(e.left), right: r(e.right) };
|
|
@@ -1567,32 +1582,38 @@ function extractStmts(stmts) {
|
|
|
1567
1582
|
// nested loop stays put.
|
|
1568
1583
|
const stripExitBreaks = (b) => b.filter(st => st.kind !== "break");
|
|
1569
1584
|
const isExit = (st) => !!st && ["break", "return", "throw", "continue"].includes(st.kind);
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1585
|
+
// Resolve JS fall-through positionally: a clause's effective body is its
|
|
1586
|
+
// own statements concatenated with each following clause's statements up
|
|
1587
|
+
// to and including the first clause that ends in break/return/throw (or
|
|
1588
|
+
// the switch end). This covers both empty stacked labels (`case A: case
|
|
1589
|
+
// B: body`) and a *non-empty* case that falls through (`case A: sA; case
|
|
1590
|
+
// B: ...`) — the stripped breaks are the switch exits.
|
|
1591
|
+
const clauseInfos = s.getClauses().map(clause => {
|
|
1592
|
+
const stmts = extractStmts(clause.getStatements());
|
|
1593
|
+
return {
|
|
1594
|
+
label: Node.isCaseClause(clause)
|
|
1595
|
+
? clause.getExpression().getText().replace(/^["']|["']$/g, "")
|
|
1596
|
+
: null,
|
|
1597
|
+
stmts,
|
|
1598
|
+
exits: isExit(stmts[stmts.length - 1]),
|
|
1599
|
+
};
|
|
1600
|
+
});
|
|
1601
|
+
const fallThroughBody = (start) => {
|
|
1602
|
+
let body = [];
|
|
1603
|
+
for (let j = start; j < clauseInfos.length; j++) {
|
|
1604
|
+
body = body.concat(clauseInfos[j].stmts);
|
|
1605
|
+
if (clauseInfos[j].exits)
|
|
1606
|
+
break;
|
|
1592
1607
|
}
|
|
1608
|
+
return stripExitBreaks(body);
|
|
1609
|
+
};
|
|
1610
|
+
for (let i = 0; i < clauseInfos.length; i++) {
|
|
1611
|
+
const c = clauseInfos[i];
|
|
1612
|
+
if (c.label === null)
|
|
1613
|
+
defaultBody = fallThroughBody(i);
|
|
1614
|
+
else
|
|
1615
|
+
cases.push({ label: c.label, body: fallThroughBody(i) });
|
|
1593
1616
|
}
|
|
1594
|
-
for (const l of fallthrough)
|
|
1595
|
-
cases.push({ label: l, body: [] });
|
|
1596
1617
|
result.push({ kind: "switch", expr: switchExpr, discriminant, cases, defaultBody, line });
|
|
1597
1618
|
continue;
|
|
1598
1619
|
}
|
|
@@ -1692,17 +1713,21 @@ function extractFunction(fn, parentAnnotations) {
|
|
|
1692
1713
|
}
|
|
1693
1714
|
}
|
|
1694
1715
|
function extractFunctionInner(fn, parentAnnotations) {
|
|
1695
|
-
//
|
|
1696
|
-
//
|
|
1716
|
+
// A `<T extends B>` bound is handled one of two ways. When B is a modelable
|
|
1717
|
+
// type (a record/nominal), substitute T with B — a body that reads T's fields
|
|
1718
|
+
// (e.g. `x.id`) then typechecks against B. When B is a union/intersection we
|
|
1719
|
+
// can't model as one Dafny type (e.g. `string & {}` tricks), keep T as a Dafny
|
|
1720
|
+
// type param instead; such a T must be phantom (any field read won't typecheck).
|
|
1697
1721
|
_typeParamMap = new Map();
|
|
1698
1722
|
_reservedForCounterNames = new Set();
|
|
1699
|
-
const
|
|
1723
|
+
const typeParams = [];
|
|
1700
1724
|
for (const tp of fn.getTypeParameters?.() ?? []) {
|
|
1701
1725
|
const constraint = tp.getConstraint();
|
|
1702
|
-
|
|
1726
|
+
const ct = constraint?.getType();
|
|
1727
|
+
if (constraint && !ct?.isUnion() && !ct?.isIntersection())
|
|
1703
1728
|
_typeParamMap.set(tp.getName(), constraint.getText());
|
|
1704
1729
|
else
|
|
1705
|
-
|
|
1730
|
+
typeParams.push(tp.getName());
|
|
1706
1731
|
}
|
|
1707
1732
|
const body = fn.getBody();
|
|
1708
1733
|
// Expression-body arrow: wrap in implicit return
|
|
@@ -1731,7 +1756,7 @@ function extractFunctionInner(fn, parentAnnotations) {
|
|
|
1731
1756
|
return {
|
|
1732
1757
|
name: fn.getName?.() ?? "<anonymous>",
|
|
1733
1758
|
exported: false, // set in extractModule against the source file's export surface
|
|
1734
|
-
typeParams
|
|
1759
|
+
typeParams,
|
|
1735
1760
|
// Original TS parameter grouping, before the flatten below loses it. `defaults` carries
|
|
1736
1761
|
// each bound name's default initializer text (omitted when none) for TS-targeting consumers.
|
|
1737
1762
|
tsParams: fn.getParameters().map(p => {
|
|
@@ -1759,10 +1784,20 @@ function extractFunctionInner(fn, parentAnnotations) {
|
|
|
1759
1784
|
const nameNode = p.getNameNode();
|
|
1760
1785
|
if (Node.isObjectBindingPattern(nameNode)) {
|
|
1761
1786
|
const type = p.getType();
|
|
1787
|
+
// `//@ declare-type` types are invisible to the TS checker, so
|
|
1788
|
+
// `type.getProperty` finds nothing for them and the bindings would
|
|
1789
|
+
// collapse to `unknown`. Fall back to the declared record's field types.
|
|
1790
|
+
const declTypeName = p.getTypeNode()?.getText();
|
|
1791
|
+
const declFields = declTypeName
|
|
1792
|
+
? _synthArrayUnions?.find(d => d.name === declTypeName && d.kind === "record")?.fields
|
|
1793
|
+
: undefined;
|
|
1762
1794
|
return nameNode.getElements().map(el => {
|
|
1763
1795
|
const name = el.getName();
|
|
1764
1796
|
const propType = type.getProperty(name)?.getTypeAtLocation(p);
|
|
1765
|
-
|
|
1797
|
+
if (propType)
|
|
1798
|
+
return { name, tsType: typeToString(propType) };
|
|
1799
|
+
const declTy = declFields?.find(f => f.name === name)?.tsType;
|
|
1800
|
+
return { name, tsType: declTy ?? "unknown" };
|
|
1766
1801
|
});
|
|
1767
1802
|
}
|
|
1768
1803
|
// Syntactic union nodes go through _tsTypeFromUnionNode so synth fires
|
|
@@ -1843,15 +1878,17 @@ export function extractModule(sourceFile) {
|
|
|
1843
1878
|
// `//@ declare-type Name { f1: T1, ... }` — record form.
|
|
1844
1879
|
// `//@ declare-type Name = TsType` — alias form (e.g. `Ruleset = Rule[]`).
|
|
1845
1880
|
function parseDeclareType(body) {
|
|
1846
|
-
const recordMatch = body.match(/^(\w+)\s*\{(.+)\}$/);
|
|
1881
|
+
const recordMatch = body.match(/^(\w+)\s*(?:<([^>]+)>)?\s*\{(.+)\}$/);
|
|
1847
1882
|
if (recordMatch) {
|
|
1848
1883
|
const name = recordMatch[1];
|
|
1849
|
-
|
|
1884
|
+
// `<T extends B>` → bare `T`: a Dafny type param, like the def path.
|
|
1885
|
+
const typeParams = recordMatch[2]?.split(",").map(s => s.trim().split(/\s+extends\s+/)[0].trim()).filter(Boolean);
|
|
1886
|
+
const fields = recordMatch[3].split(",").map(f => f.trim()).filter(Boolean).map(f => {
|
|
1850
1887
|
const [fname, ftype] = f.split(":").map(s => s.trim());
|
|
1851
1888
|
const synth = _synthFromTsTypeString(ftype);
|
|
1852
1889
|
return { name: fname, tsType: synth ?? ftype };
|
|
1853
1890
|
});
|
|
1854
|
-
typeDecls.push({ name, kind: "record", fields });
|
|
1891
|
+
typeDecls.push({ name, kind: "record", fields, ...(typeParams?.length ? { typeParams } : {}) });
|
|
1855
1892
|
return;
|
|
1856
1893
|
}
|
|
1857
1894
|
const aliasMatch = body.match(/^(\w+)\s*=\s*(.+)$/);
|
|
@@ -2052,7 +2089,8 @@ export function extractModule(sourceFile) {
|
|
|
2052
2089
|
const sig = f.node.getType().getCallSignatures()[0];
|
|
2053
2090
|
if (!sig)
|
|
2054
2091
|
continue;
|
|
2055
|
-
|
|
2092
|
+
// Bare names, dropping `extends B` — same as the def path.
|
|
2093
|
+
const typeParams = sig.getTypeParameters().map(tp => tp.getText().split(/\s+extends\s+/)[0].trim());
|
|
2056
2094
|
// Normalize via typeToString (not raw getText): resolves declare-type
|
|
2057
2095
|
// shadows and yields bare names, so a param typed by an unreachable import
|
|
2058
2096
|
// becomes `AgentMessage`, not `import("/abs/path").AgentMessage`.
|
|
@@ -2297,6 +2335,10 @@ export function extractModule(sourceFile) {
|
|
|
2297
2335
|
collectNamesExpr(e.spread);
|
|
2298
2336
|
e.fields.forEach(f => collectNamesExpr(f.value));
|
|
2299
2337
|
}
|
|
2338
|
+
if (e.kind === "recordMerge") {
|
|
2339
|
+
collectNamesExpr(e.base);
|
|
2340
|
+
collectNamesExpr(e.override);
|
|
2341
|
+
}
|
|
2300
2342
|
if (e.kind === "arrayLiteral") {
|
|
2301
2343
|
e.elems.forEach(collectNamesExpr);
|
|
2302
2344
|
}
|
|
@@ -2305,6 +2347,31 @@ export function extractModule(sourceFile) {
|
|
|
2305
2347
|
collectNamesExpr(e.then);
|
|
2306
2348
|
collectNamesExpr(e.else);
|
|
2307
2349
|
}
|
|
2350
|
+
if (e.kind === "nullish") {
|
|
2351
|
+
collectNamesExpr(e.left);
|
|
2352
|
+
collectNamesExpr(e.right);
|
|
2353
|
+
}
|
|
2354
|
+
if (e.kind === "nonNull") {
|
|
2355
|
+
collectNamesExpr(e.expr);
|
|
2356
|
+
}
|
|
2357
|
+
if (e.kind === "optChain") {
|
|
2358
|
+
collectNamesExpr(e.obj);
|
|
2359
|
+
for (const c of e.chain) {
|
|
2360
|
+
if (c.kind === "call")
|
|
2361
|
+
c.args.forEach(collectNamesExpr);
|
|
2362
|
+
if (c.kind === "index")
|
|
2363
|
+
collectNamesExpr(c.idx);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (e.kind === "lambda") {
|
|
2367
|
+
if (Array.isArray(e.body))
|
|
2368
|
+
collectNames(e.body);
|
|
2369
|
+
else
|
|
2370
|
+
collectNamesExpr(e.body);
|
|
2371
|
+
}
|
|
2372
|
+
if (e.kind === "forall" || e.kind === "exists") {
|
|
2373
|
+
collectNamesExpr(e.body);
|
|
2374
|
+
}
|
|
2308
2375
|
}
|
|
2309
2376
|
// Signature types (params + return) get base-name stripping below; body /
|
|
2310
2377
|
// spec references stay exact-match (so a body `let xs: Hunk[]` doesn't pull
|
package/tools/dist/narrow.js
CHANGED
|
@@ -100,7 +100,7 @@ const parseSimpleOptionalCheck = parseOptionalCheck;
|
|
|
100
100
|
// ── Walkers ──────────────────────────────────────────────────
|
|
101
101
|
function walkExpr(e) {
|
|
102
102
|
const r = recurseExpr(e);
|
|
103
|
-
return ruleNullish(r) ?? ruleOptChain(r) ?? ruleImplOptional(r) ?? ruleImplArrayIsArray(r) ?? ruleConditionalArrayIsArray(r) ?? ruleConditionalAndArrayIsArray(r) ?? ruleConditionalAndOptional(r) ?? ruleConditionalOptionalSimple(r) ?? ruleConditionalInMap(r) ?? ruleConditionalOptionalTruthy(r) ?? r;
|
|
103
|
+
return ruleNullish(r) ?? ruleNullishIndex(r) ?? ruleOptChainIndex(r) ?? ruleOptChain(r) ?? ruleImplOptional(r) ?? ruleImplArrayIsArray(r) ?? ruleConditionalArrayIsArray(r) ?? ruleConditionalAndArrayIsArray(r) ?? ruleConditionalAndOptional(r) ?? ruleConditionalOptionalSimple(r) ?? ruleConditionalInMap(r) ?? ruleConditionalOptionalTruthy(r) ?? r;
|
|
104
104
|
}
|
|
105
105
|
function recurseExpr(e) {
|
|
106
106
|
const re = walkExpr;
|
|
@@ -434,6 +434,61 @@ function ruleNullish(e) {
|
|
|
434
434
|
ty: e.ty,
|
|
435
435
|
};
|
|
436
436
|
}
|
|
437
|
+
/** Rule (expression): `arr[i] ?? right` — nullish coalescing on an array index.
|
|
438
|
+
* Under noUncheckedIndexedAccess `arr[i]` is `T | undefined`, undefined exactly
|
|
439
|
+
* when out of bounds, so → `(0 <= i && i < arr.length) ? arr[i] : right`. The
|
|
440
|
+
* guarded `then` keeps the seq index in bounds for the backend. (Map index is
|
|
441
|
+
* already optional-typed and handled by ruleNullish above; this is the array
|
|
442
|
+
* case, whose element type stays non-optional in expression position.) */
|
|
443
|
+
function ruleNullishIndex(e) {
|
|
444
|
+
if (e.kind !== "nullish")
|
|
445
|
+
return null;
|
|
446
|
+
if (e.left.kind !== "index")
|
|
447
|
+
return null;
|
|
448
|
+
if (e.left.obj.ty.kind !== "array")
|
|
449
|
+
return null;
|
|
450
|
+
const idx = e.left.idx;
|
|
451
|
+
const len = { kind: "field", obj: e.left.obj, field: "length", ty: { kind: "int" } };
|
|
452
|
+
const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
|
|
453
|
+
const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
|
|
454
|
+
const cond = { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
|
|
455
|
+
return { kind: "conditional", cond, then: e.left, else: e.right, ty: e.ty };
|
|
456
|
+
}
|
|
457
|
+
/** Rule (expression): `arr[i]?.<chain>` — optional chaining on an array index,
|
|
458
|
+
* the optChain sibling of ruleNullishIndex. `arr[i]` is `T | undefined`,
|
|
459
|
+
* undefined exactly out of bounds, so → `(0 <= i && i < arr.length) ? <chain on
|
|
460
|
+
* arr[i]> : undefined`. The conditional's optional type makes transform wrap the
|
|
461
|
+
* in-bounds chain result in Some and the OOB branch in None — the same Option<…>
|
|
462
|
+
* a directly-optional scrutinee yields via ruleOptChain, just bounds-guarded.
|
|
463
|
+
* (ruleOptChain itself bails here: an array index is typed as the non-optional
|
|
464
|
+
* element type, so its `?.` never reaches that rule.) */
|
|
465
|
+
function ruleOptChainIndex(e) {
|
|
466
|
+
if (e.kind !== "optChain")
|
|
467
|
+
return null;
|
|
468
|
+
if (e.obj.kind !== "index")
|
|
469
|
+
return null;
|
|
470
|
+
if (e.obj.obj.ty.kind !== "array")
|
|
471
|
+
return null;
|
|
472
|
+
const idx = e.obj.idx;
|
|
473
|
+
const len = { kind: "field", obj: e.obj.obj, field: "length", ty: { kind: "int" } };
|
|
474
|
+
const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
|
|
475
|
+
const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
|
|
476
|
+
const cond = { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
|
|
477
|
+
let body = e.obj; // arr[i] — in bounds under `cond`
|
|
478
|
+
for (const step of e.chain) {
|
|
479
|
+
if (step.kind === "field") {
|
|
480
|
+
body = { kind: "field", obj: body, field: step.name, ty: step.ty };
|
|
481
|
+
}
|
|
482
|
+
else if (step.kind === "index") {
|
|
483
|
+
body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const undef = { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
490
|
+
return { kind: "conditional", cond, then: body, else: undef, ty: e.ty };
|
|
491
|
+
}
|
|
437
492
|
/** Rule (expression): `obj?.<chain>` — single-eval optional chain.
|
|
438
493
|
* → `someMatch obj { Some(_oc{N}_val) => apply(chain, _oc{N}_val), None => undefined }`.
|
|
439
494
|
* The someBody applies the chain to the binder directly (field/call/index),
|
package/tools/dist/resolve.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* No mutation — each let extends the chain, lookup walks it.
|
|
6
6
|
*/
|
|
7
7
|
import { isBigInt } from "./typedir.js";
|
|
8
|
-
import { parseTsType } from "./types.js";
|
|
8
|
+
import { parseTsType, tyToCanonical } from "./types.js";
|
|
9
9
|
import { parseExpr } from "./specparser.js";
|
|
10
10
|
function lookup(env, name) {
|
|
11
11
|
if (!env)
|
|
@@ -349,6 +349,14 @@ function isUnmodeledTy(ty, typeDecls) {
|
|
|
349
349
|
}
|
|
350
350
|
return false;
|
|
351
351
|
}
|
|
352
|
+
/** A `user` type that resolves to a string-union declare-type — runs as a plain
|
|
353
|
+
* string at runtime, so it's a refinement of `string`, not an opaque blob. */
|
|
354
|
+
function isStringUnionTy(ty, typeDecls) {
|
|
355
|
+
if (ty.kind !== "user")
|
|
356
|
+
return false;
|
|
357
|
+
const base = ty.name.includes("<") ? ty.name.slice(0, ty.name.indexOf("<")) : ty.name;
|
|
358
|
+
return typeDecls.some(d => d.name === base && d.kind === "string-union");
|
|
359
|
+
}
|
|
352
360
|
/** Infer quantifier variable type from usage in body.
|
|
353
361
|
* If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
|
|
354
362
|
* return the collection's key type. Otherwise return null (default to int). */
|
|
@@ -457,6 +465,16 @@ function tyToTsStr(ty) {
|
|
|
457
465
|
return undefined;
|
|
458
466
|
}
|
|
459
467
|
function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
468
|
+
// sort's comparator takes two params, both the element type.
|
|
469
|
+
if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "sort" &&
|
|
470
|
+
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" && rawArgs[0].params.length >= 1) {
|
|
471
|
+
const tsType = tyToTsStr(fn.obj.ty.elem);
|
|
472
|
+
if (tsType) {
|
|
473
|
+
const lam = rawArgs[0];
|
|
474
|
+
const updatedParams = lam.params.map(p => (p.tsType ? p : { ...p, tsType }));
|
|
475
|
+
return [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
460
478
|
if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
461
479
|
["map", "filter", "every", "some", "find", "findLast", "findIndex"].includes(fn.field) &&
|
|
462
480
|
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
|
|
@@ -573,7 +591,9 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
573
591
|
return objTy.elem;
|
|
574
592
|
if (fn.field === "pop")
|
|
575
593
|
return { kind: "optional", inner: objTy.elem };
|
|
576
|
-
if (fn.field === "push" || fn.field === "concat")
|
|
594
|
+
if (fn.field === "push" || fn.field === "unshift" || fn.field === "concat")
|
|
595
|
+
return objTy;
|
|
596
|
+
if (fn.field === "sort")
|
|
577
597
|
return objTy;
|
|
578
598
|
if (fn.field === "filter")
|
|
579
599
|
return objTy;
|
|
@@ -639,6 +659,106 @@ function lookupFieldTy(objTy, field, ctx) {
|
|
|
639
659
|
return { ty: { kind: "unknown" }, isDiscriminant: false };
|
|
640
660
|
}
|
|
641
661
|
// ── Resolve expressions ──────────────────────────────────────
|
|
662
|
+
// Fresh-binder counter for the someMatches synthesized by object-spread merge.
|
|
663
|
+
let mergeBinder = 0;
|
|
664
|
+
/** Expand `{ ...base, ...override }` into a faithful field-wise merge. Driven by
|
|
665
|
+
* the result record type's fields: an override field wins when present, else the
|
|
666
|
+
* base's field shows through. Optional fields decide presence at runtime
|
|
667
|
+
* (`Some?`); an `Option`-typed override is the whole merge guarded by its tag. */
|
|
668
|
+
function resolveRecordMerge(base, override, ctx) {
|
|
669
|
+
const tbase = resolveExpr(base, ctx);
|
|
670
|
+
const tover = resolveExpr(override, ctx);
|
|
671
|
+
const overInner = tover.ty.kind === "optional" ? tover.ty.inner : tover.ty;
|
|
672
|
+
// Result record type: prefer the override's (an optional override still merges
|
|
673
|
+
// into its inner type), else the base's.
|
|
674
|
+
const rTy = overInner.kind === "user" ? overInner
|
|
675
|
+
: tbase.ty.kind === "user" ? tbase.ty : null;
|
|
676
|
+
const decl = rTy ? ctx.typeDecls.find(d => d.name === rTy.name && d.kind === "record") : undefined;
|
|
677
|
+
if (!rTy || !decl?.fields) {
|
|
678
|
+
throw new Error(`object spread merge { ...a, ...b } needs a known record type for both operands ` +
|
|
679
|
+
`(base: ${tyToCanonical(tbase.ty)}, override: ${tyToCanonical(tover.ty)})`);
|
|
680
|
+
}
|
|
681
|
+
if (tbase.ty.kind === "optional") {
|
|
682
|
+
throw new Error(`object spread merge with an optional base operand is not supported (base: ${tyToCanonical(tbase.ty)})`);
|
|
683
|
+
}
|
|
684
|
+
const userTy = rTy;
|
|
685
|
+
const fields = decl.fields;
|
|
686
|
+
// Build the merged literal from concrete base/override values, both : userTy.
|
|
687
|
+
const merged = (bv, ov) => ({
|
|
688
|
+
kind: "record", spread: null, ty: userTy,
|
|
689
|
+
fields: fields.map(f => {
|
|
690
|
+
const ft = f.type;
|
|
691
|
+
const ovf = { kind: "field", obj: ov, field: f.name, ty: ft };
|
|
692
|
+
if (ft.kind !== "optional")
|
|
693
|
+
return { name: f.name, value: ovf }; // required: override always provides
|
|
694
|
+
// optional: override field wins iff present, else base's field
|
|
695
|
+
const bvf = { kind: "field", obj: bv, field: f.name, ty: ft };
|
|
696
|
+
const binder = `_m${mergeBinder++}`;
|
|
697
|
+
// someBody is the unwrapped present value; transform re-wraps each arm in
|
|
698
|
+
// the backend's Some constructor (Dafny `Some`, Lean `Option.some`).
|
|
699
|
+
return { name: f.name, value: {
|
|
700
|
+
kind: "someMatch", scrutinee: ovf, binder, binderTy: ft.inner,
|
|
701
|
+
someBody: { kind: "var", name: binder, ty: ft.inner }, noneBody: bvf, ty: ft,
|
|
702
|
+
} };
|
|
703
|
+
}),
|
|
704
|
+
});
|
|
705
|
+
if (tover.ty.kind === "optional") {
|
|
706
|
+
// override may be absent (undefined spreads nothing) → base unchanged
|
|
707
|
+
const binder = `_mo${mergeBinder++}`;
|
|
708
|
+
return {
|
|
709
|
+
kind: "someMatch", scrutinee: tover, binder, binderTy: userTy,
|
|
710
|
+
someBody: merged(tbase, { kind: "var", name: binder, ty: userTy }), noneBody: tbase, ty: userTy,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
return merged(tbase, tover);
|
|
714
|
+
}
|
|
715
|
+
/** `rec[k]` where `rec` is a record and `k` an enum of its field names. A *named*
|
|
716
|
+
* string-union key is a datatype → `match k { case f => rec.f }`; an *inline*
|
|
717
|
+
* union (`"a" | "b"`, a bare string carrying its members) → an equality chain
|
|
718
|
+
* `if k === "a" then rec.a else …`. Either way the chain/match covers exactly
|
|
719
|
+
* the key's values, so a subset key stays sound. Returns null if the shape
|
|
720
|
+
* doesn't apply (caller falls back to plain index). */
|
|
721
|
+
function tryRecordIndexByEnum(obj, idx, ctx) {
|
|
722
|
+
const objTy = obj.ty, keyTy = idx.ty;
|
|
723
|
+
if (objTy.kind !== "user")
|
|
724
|
+
return null;
|
|
725
|
+
const rec = ctx.typeDecls.find(d => d.name === objTy.name && d.kind === "record");
|
|
726
|
+
if (!rec?.fields)
|
|
727
|
+
return null;
|
|
728
|
+
const fieldByName = new Map(rec.fields.map(f => [f.name, f]));
|
|
729
|
+
const fieldTy = (v) => fieldByName.get(v).type ?? { kind: "unknown" };
|
|
730
|
+
const field = (v) => ({ kind: "field", obj, field: v, ty: fieldTy(v) });
|
|
731
|
+
// The key's members, and whether it's a datatype (named) or a bare string (inline).
|
|
732
|
+
let values = null;
|
|
733
|
+
let datatype = null;
|
|
734
|
+
if (keyTy.kind === "user") {
|
|
735
|
+
const keyEnum = ctx.typeDecls.find(d => d.name === keyTy.name && d.kind === "string-union");
|
|
736
|
+
if (keyEnum?.values?.length) {
|
|
737
|
+
values = keyEnum.values;
|
|
738
|
+
datatype = keyEnum.name;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
else if (keyTy.kind === "string" && keyTy.values?.length) {
|
|
742
|
+
values = keyTy.values;
|
|
743
|
+
}
|
|
744
|
+
if (!values || !values.every(v => fieldByName.has(v)))
|
|
745
|
+
return null; // key isn't a subset of fields
|
|
746
|
+
if (datatype) {
|
|
747
|
+
return {
|
|
748
|
+
kind: "tagMatch", scrutinee: idx, typeName: datatype,
|
|
749
|
+
cases: values.map(v => ({ variant: v, body: field(v) })), fallthrough: null, ty: fieldTy(values[0]),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
// Inline union: fold right into an equality chain, last member as the bare else.
|
|
753
|
+
let expr = field(values[values.length - 1]);
|
|
754
|
+
for (let i = values.length - 2; i >= 0; i--) {
|
|
755
|
+
expr = {
|
|
756
|
+
kind: "conditional", ty: fieldTy(values[i]), then: field(values[i]), else: expr,
|
|
757
|
+
cond: { kind: "binop", op: "===", left: idx, right: { kind: "str", value: values[i], ty: { kind: "string" } }, ty: { kind: "bool" } },
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return expr;
|
|
761
|
+
}
|
|
642
762
|
function resolveExpr(e, ctx) {
|
|
643
763
|
switch (e.kind) {
|
|
644
764
|
case "var":
|
|
@@ -812,6 +932,9 @@ function resolveExpr(e, ctx) {
|
|
|
812
932
|
idxTy = narrowed ? obj.ty.value : { kind: "optional", inner: obj.ty.value };
|
|
813
933
|
}
|
|
814
934
|
else {
|
|
935
|
+
const recIdx = tryRecordIndexByEnum(obj, idx, ctx);
|
|
936
|
+
if (recIdx)
|
|
937
|
+
return recIdx;
|
|
815
938
|
idxTy = { kind: "unknown" };
|
|
816
939
|
}
|
|
817
940
|
return { kind: "index", obj, idx, ty: idxTy };
|
|
@@ -842,8 +965,10 @@ function resolveExpr(e, ctx) {
|
|
|
842
965
|
// left ?? right — result type is left's inner (when left is optional)
|
|
843
966
|
// or just left's type, unified with right's type.
|
|
844
967
|
const left = resolveExpr(e.left, ctx);
|
|
845
|
-
const right = resolveExpr(e.right, ctx);
|
|
846
968
|
const ty = left.ty.kind === "optional" ? left.ty.inner : left.ty;
|
|
969
|
+
// The default shares the result type, so coerce a string literal to a
|
|
970
|
+
// string-union enum (e.g. `availableLevels[0] ?? "off"`).
|
|
971
|
+
const right = coerceStr(resolveExpr(e.right, ctx), ty);
|
|
847
972
|
return { kind: "nullish", left, right, ty };
|
|
848
973
|
}
|
|
849
974
|
case "optChain": {
|
|
@@ -944,6 +1069,8 @@ function resolveExpr(e, ctx) {
|
|
|
944
1069
|
});
|
|
945
1070
|
return { kind: "record", spread, fields, ty: recordTy ?? ty };
|
|
946
1071
|
}
|
|
1072
|
+
case "recordMerge":
|
|
1073
|
+
return resolveRecordMerge(e.base, e.override, ctx);
|
|
947
1074
|
case "result":
|
|
948
1075
|
// \result desugars to a regular var so all the variable-narrowing
|
|
949
1076
|
// machinery (env lookup, optional checks, path matching) just works.
|
|
@@ -967,8 +1094,14 @@ function resolveExpr(e, ctx) {
|
|
|
967
1094
|
// anonymous tuple (mirrors return-position and call-argument records, which
|
|
968
1095
|
// get their type via ctx.returnTy). Only narrow when the context type is an
|
|
969
1096
|
// array; otherwise leave ctx untouched.
|
|
970
|
-
const
|
|
971
|
-
const
|
|
1097
|
+
const expectedElem = ctx.returnTy.kind === "array" ? ctx.returnTy.elem : null;
|
|
1098
|
+
const elemCtx = expectedElem ? { ...ctx, returnTy: expectedElem } : ctx;
|
|
1099
|
+
const elems = e.elems.map(el => {
|
|
1100
|
+
const r = resolveExpr(el, elemCtx);
|
|
1101
|
+
// Coerce a bare string-literal element to a string-union enum (e.g.
|
|
1102
|
+
// `["off", …]: ModelThinkingLevel[]`), like return/arg positions.
|
|
1103
|
+
return expectedElem ? coerceStr(r, expectedElem) : r;
|
|
1104
|
+
});
|
|
972
1105
|
const elemTy = elems.length > 0 ? elems[0].ty : { kind: "unknown" };
|
|
973
1106
|
return { kind: "arrayLiteral", elems, ty: { kind: "array", elem: elemTy } };
|
|
974
1107
|
}
|
|
@@ -1140,6 +1273,11 @@ function resolveStmt(s, ctx) {
|
|
|
1140
1273
|
? { kind: "optional", inner: init.ty }
|
|
1141
1274
|
: init.ty;
|
|
1142
1275
|
}
|
|
1276
|
+
else if (declTy.kind === "string" && isStringUnionTy(init.ty, ctx.typeDecls) && !ctx.overrides.has(s.name)) {
|
|
1277
|
+
// ts-morph widened a string-union to `string`; keep the initializer's
|
|
1278
|
+
// datatype so `local === "lit"` lowers to a discriminant test.
|
|
1279
|
+
ty = init.ty;
|
|
1280
|
+
}
|
|
1143
1281
|
else if ((declTy.kind === "int" || declTy.kind === "nat") && init.ty.kind === "real" && !ctx.overrides.has(s.name)) {
|
|
1144
1282
|
// TS infers `number` (→ int/nat) for an expression LS computes as `real`
|
|
1145
1283
|
// (e.g. `a / b`, now real division). `number` can't tell them apart, so
|
|
@@ -1332,6 +1470,10 @@ function collectCallsExpr(e, fns, out) {
|
|
|
1332
1470
|
for (const f of e.fields)
|
|
1333
1471
|
collectCallsExpr(f.value, fns, out);
|
|
1334
1472
|
return;
|
|
1473
|
+
case "recordMerge":
|
|
1474
|
+
collectCallsExpr(e.base, fns, out);
|
|
1475
|
+
collectCallsExpr(e.override, fns, out);
|
|
1476
|
+
return;
|
|
1335
1477
|
case "arrayLiteral":
|
|
1336
1478
|
for (const el of e.elems)
|
|
1337
1479
|
collectCallsExpr(el, fns, out);
|
package/tools/dist/transform.js
CHANGED
|
@@ -1359,7 +1359,7 @@ function transformStmt(s, typeDecls) {
|
|
|
1359
1359
|
const recv = s.expr.fn.obj;
|
|
1360
1360
|
const f = s.expr.fn.field;
|
|
1361
1361
|
const isMutating = ((recv.ty.kind === "map" || recv.ty.kind === "set") && (f === "set" || f === "add" || f === "delete")) ||
|
|
1362
|
-
(recv.ty.kind === "array" && f === "push");
|
|
1362
|
+
(recv.ty.kind === "array" && (f === "push" || f === "unshift" || f === "sort"));
|
|
1363
1363
|
if (isMutating && recv.kind === "var") {
|
|
1364
1364
|
const { binds, expr } = liftMethodCalls(s.expr);
|
|
1365
1365
|
return [...binds, { kind: "assign", target: recv.name, value: expr }];
|
|
@@ -1836,6 +1836,7 @@ function transformTypeDecl(d) {
|
|
|
1836
1836
|
else {
|
|
1837
1837
|
return {
|
|
1838
1838
|
kind: "structure", name: d.name,
|
|
1839
|
+
typeParams: d.typeParams,
|
|
1839
1840
|
fields: d.fields.map(f => ({ name: f.name, type: f.type })),
|
|
1840
1841
|
deriving: ["Repr", "Inhabited", "DecidableEq"],
|
|
1841
1842
|
};
|
|
@@ -1854,7 +1855,7 @@ function findReassignedNames(stmts, names) {
|
|
|
1854
1855
|
// Mutating collection calls: s.add(x), m.set(k,v), s.delete(x), arr.push(x)
|
|
1855
1856
|
if (s.kind === "expr" && s.expr.kind === "call" && s.expr.fn.kind === "field" &&
|
|
1856
1857
|
s.expr.fn.obj.kind === "var" && names.has(s.expr.fn.obj.name) &&
|
|
1857
|
-
["add", "set", "delete", "push"].includes(s.expr.fn.field)) {
|
|
1858
|
+
["add", "set", "delete", "push", "unshift"].includes(s.expr.fn.field)) {
|
|
1858
1859
|
found.add(s.expr.fn.obj.name);
|
|
1859
1860
|
}
|
|
1860
1861
|
if (s.kind === "if") {
|
package/tools/dist/types.js
CHANGED
|
@@ -55,6 +55,20 @@ function tyFromTypeNode(tn) {
|
|
|
55
55
|
if (normalized.length === 1 && "syntheticBool" in normalized[0])
|
|
56
56
|
return { kind: "bool" };
|
|
57
57
|
const nonNullish = normalized.filter(a => "syntheticBool" in a || !isNullish(a.node));
|
|
58
|
+
// Inline string-literal union (`"a" | "b"`, not a //@ declare-type): no datatype
|
|
59
|
+
// to resolve against, so lower to plain string (the arms are strings; == holds).
|
|
60
|
+
const isStrLit = (a) => !("syntheticBool" in a) && Node.isLiteralTypeNode(a.node) && a.node.getLiteral().getKind() === SyntaxKind.StringLiteral;
|
|
61
|
+
if (nonNullish.length >= 2 && nonNullish.every(isStrLit)) {
|
|
62
|
+
// Keep the literal members so `rec[k]` can lower to an equality chain.
|
|
63
|
+
const values = nonNullish.map(a => {
|
|
64
|
+
const lit = a.node;
|
|
65
|
+
const inner = Node.isLiteralTypeNode(lit) ? lit.getLiteral() : lit;
|
|
66
|
+
return Node.isStringLiteral(inner) ? inner.getLiteralValue() : inner.getText();
|
|
67
|
+
});
|
|
68
|
+
return normalized.some(a => !("syntheticBool" in a) && isNullish(a.node))
|
|
69
|
+
? { kind: "optional", inner: { kind: "string", values } }
|
|
70
|
+
: { kind: "string", values };
|
|
71
|
+
}
|
|
58
72
|
if (nonNullish.length === 1 && normalized.length >= 2) {
|
|
59
73
|
const sole = nonNullish[0];
|
|
60
74
|
const inner = "syntheticBool" in sole ? { kind: "bool" } : tyFromTypeNode(sole.node);
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `lsc guard` — emit a drop-in `<file>.guarded.ts` that enforces each verified
|
|
3
|
-
* function's `//@ requires` at runtime.
|
|
4
|
-
*
|
|
5
|
-
* Backend-neutral (like `extract`/`info`): reads the Raw IR and re-parses each
|
|
6
|
-
* `//@ requires` string with the specparser, then lowers the resulting RawExpr
|
|
7
|
-
* back to executable TypeScript. The generated module re-exports every function
|
|
8
|
-
* at its original signature, each guarded: on a violated clause it throws
|
|
9
|
-
* `PreconditionError(fn, clause, clauseId, args, detail)`; a `can.*` namespace
|
|
10
|
-
* exposes the same per-clause checks as booleans for render-time gating.
|
|
11
|
-
*
|
|
12
|
-
* Clauses naming a symbol that is not TS-resident (a ghost `.dfy` predicate, an
|
|
13
|
-
* unbounded quantifier) cannot be lowered — they are SKIPPED with a warning,
|
|
14
|
-
* never faked. Internal core-to-core calls stay raw (`__core.*`); the proof
|
|
15
|
-
* covers those.
|
|
16
|
-
*/
|
|
17
|
-
import { writeFileSync } from "fs";
|
|
18
|
-
import * as path from "path";
|
|
19
|
-
import { parseExpr } from "./specparser.js";
|
|
20
|
-
class NotLowerable extends Error {
|
|
21
|
-
}
|
|
22
|
-
const GLOBALS = new Set(["Math", "Number", "undefined"]);
|
|
23
|
-
// ── RawExpr → executable TS (throws NotLowerable on a non-TS-resident symbol) ──
|
|
24
|
-
function lower(e, ctx) {
|
|
25
|
-
switch (e.kind) {
|
|
26
|
-
case "num": return String(e.value);
|
|
27
|
-
case "bool": return String(e.value);
|
|
28
|
-
case "str": return JSON.stringify(e.value);
|
|
29
|
-
case "var":
|
|
30
|
-
if (ctx.bound.has(e.name) || ctx.params.has(e.name) || GLOBALS.has(e.name))
|
|
31
|
-
return e.name;
|
|
32
|
-
if (ctx.fns.has(e.name))
|
|
33
|
-
return `__core.${e.name}`;
|
|
34
|
-
throw new NotLowerable(`unknown symbol '${e.name}' (not a param, bound var, or module function)`);
|
|
35
|
-
case "field": return `${lower(e.obj, ctx)}.${e.field}`;
|
|
36
|
-
case "index": return `${lower(e.obj, ctx)}[${lower(e.idx, ctx)}]`;
|
|
37
|
-
case "call": return `${lower(e.fn, ctx)}(${e.args.map((a) => lower(a, ctx)).join(", ")})`;
|
|
38
|
-
case "unop": return `(${e.op}${lower(e.expr, ctx)})`;
|
|
39
|
-
case "conditional":
|
|
40
|
-
return `(${lower(e.cond, ctx)} ? ${lower(e.then, ctx)} : ${lower(e.else, ctx)})`;
|
|
41
|
-
case "arrayLiteral": return `[${e.elems.map((x) => lower(x, ctx)).join(", ")}]`;
|
|
42
|
-
case "binop": {
|
|
43
|
-
const l = () => lower(e.left, ctx), r = () => lower(e.right, ctx);
|
|
44
|
-
if (e.op === "==>")
|
|
45
|
-
return `(!(${l()}) || (${r()}))`;
|
|
46
|
-
if (e.op === "<==>")
|
|
47
|
-
return `((${l()}) === (${r()}))`;
|
|
48
|
-
if (e.op === "in")
|
|
49
|
-
throw new NotLowerable("'in' membership not yet lowered");
|
|
50
|
-
return `(${l()} ${e.op} ${r()})`;
|
|
51
|
-
}
|
|
52
|
-
case "forall":
|
|
53
|
-
case "exists": {
|
|
54
|
-
const { lo, ubOp, ub } = quantRange(e, ctx);
|
|
55
|
-
const inner = { ...ctx, bound: new Set([...ctx.bound, e.var]) };
|
|
56
|
-
const body = lower(e.body, inner);
|
|
57
|
-
const hit = e.kind === "forall" ? `!(${body})` : `(${body})`;
|
|
58
|
-
const found = e.kind === "forall" ? "false" : "true";
|
|
59
|
-
const dflt = e.kind === "forall" ? "true" : "false";
|
|
60
|
-
return `(() => { for (let ${e.var} = ${lo}; ${e.var} ${ubOp} ${ub}; ${e.var}++) { if (${hit}) return ${found}; } return ${dflt}; })()`;
|
|
61
|
-
}
|
|
62
|
-
default: throw new NotLowerable(`unsupported expression kind '${e.kind}'`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// Extract a sound finite iteration range [lo, ub) for the quantified var from a
|
|
66
|
-
// `lo <= v && v < ub ==> P` antecedent. Both bounds must be found (or var is a
|
|
67
|
-
// nat) — guessing would risk an unsound under-scan. Throws otherwise.
|
|
68
|
-
function quantRange(q, ctx) {
|
|
69
|
-
if (q.body.kind !== "binop" || q.body.op !== "==>")
|
|
70
|
-
throw new NotLowerable(`quantifier over '${q.var}' has no bounding antecedent`);
|
|
71
|
-
const inner = { ...ctx, bound: new Set([...ctx.bound, q.var]) };
|
|
72
|
-
const isVar = (x) => x.kind === "var" && x.name === q.var;
|
|
73
|
-
const conj = (e) => e.kind === "binop" && e.op === "&&" ? [...conj(e.left), ...conj(e.right)] : [e];
|
|
74
|
-
let upper = null;
|
|
75
|
-
let lowerB = null;
|
|
76
|
-
for (const c of conj(q.body.left)) {
|
|
77
|
-
if (c.kind !== "binop")
|
|
78
|
-
continue;
|
|
79
|
-
if (isVar(c.left) && (c.op === "<" || c.op === "<="))
|
|
80
|
-
upper = { ub: lower(c.right, inner), strict: c.op === "<" };
|
|
81
|
-
else if (isVar(c.right) && (c.op === ">" || c.op === ">="))
|
|
82
|
-
upper = { ub: lower(c.left, inner), strict: c.op === ">" };
|
|
83
|
-
else if (isVar(c.right) && (c.op === "<" || c.op === "<="))
|
|
84
|
-
lowerB = { expr: lower(c.left, inner), strict: c.op === "<" };
|
|
85
|
-
else if (isVar(c.left) && (c.op === ">" || c.op === ">="))
|
|
86
|
-
lowerB = { expr: lower(c.right, inner), strict: c.op === ">" };
|
|
87
|
-
}
|
|
88
|
-
if (!upper)
|
|
89
|
-
throw new NotLowerable(`no upper bound found for '${q.var}'`);
|
|
90
|
-
let lo;
|
|
91
|
-
if (lowerB)
|
|
92
|
-
lo = lowerB.strict ? `(${lowerB.expr}) + 1` : lowerB.expr;
|
|
93
|
-
else if (q.varType === "nat")
|
|
94
|
-
lo = "0";
|
|
95
|
-
else
|
|
96
|
-
throw new NotLowerable(`no lower bound found for '${q.var}'`);
|
|
97
|
-
return { lo, ubOp: upper.strict ? "<" : "<=", ub: upper.ub };
|
|
98
|
-
}
|
|
99
|
-
// ── human-readable label for a sub-expression (spec text, bare names) ──
|
|
100
|
-
function render(e) {
|
|
101
|
-
switch (e.kind) {
|
|
102
|
-
case "num": return String(e.value);
|
|
103
|
-
case "bool": return String(e.value);
|
|
104
|
-
case "str": return JSON.stringify(e.value);
|
|
105
|
-
case "var": return e.name;
|
|
106
|
-
case "field": return `${render(e.obj)}.${e.field}`;
|
|
107
|
-
case "index": return `${render(e.obj)}[${render(e.idx)}]`;
|
|
108
|
-
case "call": return `${render(e.fn)}(${e.args.map(render).join(", ")})`;
|
|
109
|
-
case "unop": return `${e.op}${render(e.expr)}`;
|
|
110
|
-
case "binop": return `${render(e.left)} ${e.op} ${render(e.right)}`;
|
|
111
|
-
case "conditional": return `${render(e.cond)} ? ${render(e.then)} : ${render(e.else)}`;
|
|
112
|
-
default: return "?";
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
// Maximal "notable" sub-expressions (calls / field / index / param vars) whose
|
|
116
|
-
// runtime values explain a failure. Recurses through operators only.
|
|
117
|
-
function collectNotable(e, ctx, out) {
|
|
118
|
-
switch (e.kind) {
|
|
119
|
-
case "call":
|
|
120
|
-
case "field":
|
|
121
|
-
case "index": {
|
|
122
|
-
try {
|
|
123
|
-
out.set(render(e), lower(e, ctx));
|
|
124
|
-
}
|
|
125
|
-
catch { /* skip unlowerable leaf */ }
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
case "var":
|
|
129
|
-
if (ctx.params.has(e.name))
|
|
130
|
-
out.set(e.name, e.name);
|
|
131
|
-
return;
|
|
132
|
-
case "binop":
|
|
133
|
-
collectNotable(e.left, ctx, out);
|
|
134
|
-
collectNotable(e.right, ctx, out);
|
|
135
|
-
return;
|
|
136
|
-
case "unop":
|
|
137
|
-
collectNotable(e.expr, ctx, out);
|
|
138
|
-
return;
|
|
139
|
-
case "conditional":
|
|
140
|
-
collectNotable(e.cond, ctx, out);
|
|
141
|
-
collectNotable(e.then, ctx, out);
|
|
142
|
-
collectNotable(e.else, ctx, out);
|
|
143
|
-
return;
|
|
144
|
-
default: return;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
// ── per-function check table ──────────────────────────────────────────
|
|
148
|
-
function buildChecks(fn, ctx) {
|
|
149
|
-
const preamble = [], entries = [], skipped = [];
|
|
150
|
-
fn.requires.forEach((src, i) => {
|
|
151
|
-
const id = `${fn.name}#${i}`;
|
|
152
|
-
try {
|
|
153
|
-
const ast = parseExpr(src);
|
|
154
|
-
const clauseLit = JSON.stringify(src);
|
|
155
|
-
if (ast.kind === "forall") {
|
|
156
|
-
const { lo, ubOp, ub } = quantRange(ast, ctx);
|
|
157
|
-
const inner = { ...ctx, bound: new Set([...ctx.bound, ast.var]) };
|
|
158
|
-
const body = lower(ast.body, inner);
|
|
159
|
-
const w = `__w${i}`;
|
|
160
|
-
preamble.push(` const ${w} = ((): number => { for (let ${ast.var} = ${lo}; ${ast.var} ${ubOp} ${ub}; ${ast.var}++) { if (!(${body})) return ${ast.var}; } return -1; })();`);
|
|
161
|
-
entries.push(` __C(${JSON.stringify(id)}, ${clauseLit}, ${w} === -1, () => ({ ${JSON.stringify(ast.var)}: ${w} })),`);
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
const ok = lower(ast, ctx);
|
|
165
|
-
const notes = new Map();
|
|
166
|
-
collectNotable(ast, ctx, notes);
|
|
167
|
-
const detail = `{ ${[...notes].map(([k, v]) => `${JSON.stringify(k)}: ${v}`).join(", ")} }`;
|
|
168
|
-
entries.push(` __C(${JSON.stringify(id)}, ${clauseLit}, (${ok}), () => (${detail})),`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
catch (err) {
|
|
172
|
-
if (!(err instanceof NotLowerable))
|
|
173
|
-
throw err;
|
|
174
|
-
skipped.push(` // SKIPPED ${id} (${err.message}): ${src}`);
|
|
175
|
-
console.warn(` warning: ${fn.name} — unlowerable clause skipped (${err.message}): ${src}`);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
return { preamble, entries, skipped };
|
|
179
|
-
}
|
|
180
|
-
function sig(fn) {
|
|
181
|
-
const tp = fn.typeParams.length ? `<${fn.typeParams.join(", ")}>` : "";
|
|
182
|
-
const params = fn.params.map((p) => `${p.name}: ${p.tsType}`).join(", ");
|
|
183
|
-
return `${tp}(${params}): ${fn.returnType}`;
|
|
184
|
-
}
|
|
185
|
-
const argList = (fn) => fn.params.map((p) => p.name).join(", ");
|
|
186
|
-
function emitFunction(fn, ctx) {
|
|
187
|
-
const { preamble, entries, skipped } = buildChecks(fn, ctx);
|
|
188
|
-
const checksBody = [...skipped, ...preamble, ` return [`, ...entries, ` ];`].join("\n");
|
|
189
|
-
const args = argList(fn);
|
|
190
|
-
return [
|
|
191
|
-
`function checks_${fn.name}(${fn.params.map((p) => `${p.name}: ${p.tsType}`).join(", ")}): __Check[] {`,
|
|
192
|
-
checksBody,
|
|
193
|
-
`}`,
|
|
194
|
-
`export function ${fn.name}${sig(fn)} {`,
|
|
195
|
-
` return __enforce(${JSON.stringify(fn.name)}, [${args}], checks_${fn.name}(${args}), () => __core.${fn.name}(${args}));`,
|
|
196
|
-
`}`,
|
|
197
|
-
].join("\n");
|
|
198
|
-
}
|
|
199
|
-
export function runGuard(raw, outPath) {
|
|
200
|
-
const base = path.basename(raw.file, ".ts");
|
|
201
|
-
const fnNames = new Set(raw.functions.map((f) => f.name));
|
|
202
|
-
const blocks = raw.functions.map((fn) => {
|
|
203
|
-
const ctx = { params: new Set(fn.params.map((p) => p.name)), bound: new Set(), fns: fnNames };
|
|
204
|
-
return emitFunction(fn, ctx);
|
|
205
|
-
});
|
|
206
|
-
const canEntries = raw.functions.map((fn) => ` ${fn.name}: (${fn.params.map((p) => `${p.name}: ${p.tsType}`).join(", ")}): boolean => __holds(checks_${fn.name}(${argList(fn)})),`);
|
|
207
|
-
const header = [
|
|
208
|
-
`// ${base}.guarded.ts — GENERATED by \`lsc guard\`. Do not edit.`,
|
|
209
|
-
`// Drop-in for ${base}.ts: each function checks its //@ requires and throws`,
|
|
210
|
-
`// PreconditionError on violation; \`can.*\` runs the same checks as booleans.`,
|
|
211
|
-
``,
|
|
212
|
-
`import * as __core from "./${base}";`,
|
|
213
|
-
``,
|
|
214
|
-
`export class PreconditionError extends Error {`,
|
|
215
|
-
` constructor(`,
|
|
216
|
-
` readonly fn: string,`,
|
|
217
|
-
` readonly clause: string,`,
|
|
218
|
-
` readonly clauseId: string,`,
|
|
219
|
-
` readonly args: unknown[],`,
|
|
220
|
-
` readonly detail: unknown,`,
|
|
221
|
-
` ) {`,
|
|
222
|
-
` super(\`precondition failed in \${fn}: \${clause}\`);`,
|
|
223
|
-
` this.name = "PreconditionError";`,
|
|
224
|
-
` }`,
|
|
225
|
-
`}`,
|
|
226
|
-
``,
|
|
227
|
-
`type __Check = { id: string; clause: string; ok: boolean; detail: () => unknown };`,
|
|
228
|
-
`const __C = (id: string, clause: string, ok: boolean, detail: () => unknown): __Check => ({ id, clause, ok, detail });`,
|
|
229
|
-
`function __enforce<R>(fn: string, args: unknown[], checks: __Check[], call: () => R): R {`,
|
|
230
|
-
` for (const c of checks) if (!c.ok) throw new PreconditionError(fn, c.clause, c.id, args, c.detail());`,
|
|
231
|
-
` return call();`,
|
|
232
|
-
`}`,
|
|
233
|
-
`const __holds = (checks: __Check[]): boolean => checks.every((c) => c.ok);`,
|
|
234
|
-
].join("\n");
|
|
235
|
-
const text = [header, "", ...blocks, "", "export const can = {", ...canEntries, "};", ""].join("\n");
|
|
236
|
-
writeFileSync(outPath, text);
|
|
237
|
-
console.log(`Wrote ${outPath} (${raw.functions.length} functions guarded)`);
|
|
238
|
-
}
|