lemmascript 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -3
- package/package.json +30 -20
- package/tools/dist/dafny-commands.js +98 -0
- package/tools/dist/dafny-emit.js +738 -0
- package/tools/dist/emit.js +253 -0
- package/tools/dist/extract.js +806 -0
- package/tools/dist/ir.js +7 -0
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +393 -0
- package/tools/dist/lsc.js +119 -0
- package/tools/dist/rawir.js +10 -0
- package/tools/dist/resolve.js +717 -0
- package/tools/dist/specparser.js +305 -0
- package/tools/dist/transform.js +1091 -0
- package/tools/dist/typedir.js +7 -0
- package/tools/dist/types.js +71 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -4
- package/src/index.ts +0 -1
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract — ts-morph → Raw IR.
|
|
3
|
+
*
|
|
4
|
+
* Produces structured AST nodes, not strings.
|
|
5
|
+
* The only strings are //@ annotation expressions (parsed later by specparser).
|
|
6
|
+
*/
|
|
7
|
+
import { Project, Node, SyntaxKind, ScriptTarget } from "ts-morph";
|
|
8
|
+
// ── Expression extraction ────────────────────────────────────
|
|
9
|
+
/** When set, calls whose function/method name matches this key are replaced with havoc. */
|
|
10
|
+
let _havocKey = null;
|
|
11
|
+
function extractExpr(node) {
|
|
12
|
+
// Havoc key matching: replace matching calls with havoc expression
|
|
13
|
+
if (_havocKey && Node.isCallExpression(node)) {
|
|
14
|
+
const fnExpr = node.getExpression();
|
|
15
|
+
const name = Node.isPropertyAccessExpression(fnExpr) ? fnExpr.getName()
|
|
16
|
+
: Node.isIdentifier(fnExpr) ? fnExpr.getText()
|
|
17
|
+
: null;
|
|
18
|
+
if (name === _havocKey) {
|
|
19
|
+
return { kind: "havoc", tsType: typeToString(node.getType()) };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Numeric literal
|
|
23
|
+
if (Node.isNumericLiteral(node)) {
|
|
24
|
+
return { kind: "num", value: Number(node.getLiteralValue()) };
|
|
25
|
+
}
|
|
26
|
+
// BigInt literal (e.g. 32n, 0xffffn) — treat as integer
|
|
27
|
+
if (Node.isBigIntLiteral(node)) {
|
|
28
|
+
const text = node.getText().replace(/n$/, '');
|
|
29
|
+
return { kind: "num", value: Number(text) };
|
|
30
|
+
}
|
|
31
|
+
// Template literal: `foo${x}bar` → "foo" + x + "bar"
|
|
32
|
+
if (Node.isTemplateExpression(node)) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
const head = node.getHead().getLiteralText();
|
|
35
|
+
if (head)
|
|
36
|
+
parts.push({ kind: "str", value: head });
|
|
37
|
+
for (const span of node.getTemplateSpans()) {
|
|
38
|
+
parts.push(extractExpr(span.getExpression()));
|
|
39
|
+
const text = span.getLiteral().getLiteralText();
|
|
40
|
+
if (text)
|
|
41
|
+
parts.push({ kind: "str", value: text });
|
|
42
|
+
}
|
|
43
|
+
return parts.reduce((left, right) => ({ kind: "binop", op: "+", left, right }));
|
|
44
|
+
}
|
|
45
|
+
// No-substitution template literal: `hello` → "hello"
|
|
46
|
+
if (Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
47
|
+
return { kind: "str", value: node.getLiteralText() };
|
|
48
|
+
}
|
|
49
|
+
// String literal
|
|
50
|
+
if (Node.isStringLiteral(node)) {
|
|
51
|
+
return { kind: "str", value: node.getLiteralValue() };
|
|
52
|
+
}
|
|
53
|
+
// Boolean literals: true, false
|
|
54
|
+
if (Node.isTrueLiteral(node))
|
|
55
|
+
return { kind: "bool", value: true };
|
|
56
|
+
if (Node.isFalseLiteral(node))
|
|
57
|
+
return { kind: "bool", value: false };
|
|
58
|
+
// Identifier
|
|
59
|
+
if (Node.isIdentifier(node)) {
|
|
60
|
+
return { kind: "var", name: node.getText() };
|
|
61
|
+
}
|
|
62
|
+
// this
|
|
63
|
+
if (node.getKind() === SyntaxKind.ThisKeyword) {
|
|
64
|
+
return { kind: "var", name: "this" };
|
|
65
|
+
}
|
|
66
|
+
// Property access: x.foo
|
|
67
|
+
if (Node.isPropertyAccessExpression(node)) {
|
|
68
|
+
return { kind: "field", obj: extractExpr(node.getExpression()), field: node.getName() };
|
|
69
|
+
}
|
|
70
|
+
// Element access: arr[i]
|
|
71
|
+
if (Node.isElementAccessExpression(node)) {
|
|
72
|
+
const arg = node.getArgumentExpression();
|
|
73
|
+
if (!arg)
|
|
74
|
+
throw new Error(`Missing index in element access: ${node.getText()}`);
|
|
75
|
+
return { kind: "index", obj: extractExpr(node.getExpression()), idx: extractExpr(arg) };
|
|
76
|
+
}
|
|
77
|
+
// Call expression: f(a, b)
|
|
78
|
+
if (Node.isCallExpression(node)) {
|
|
79
|
+
return {
|
|
80
|
+
kind: "call",
|
|
81
|
+
fn: extractExpr(node.getExpression()),
|
|
82
|
+
args: node.getArguments().map(a => extractExpr(a)),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Binary expression: a + b, a === b, etc.
|
|
86
|
+
if (Node.isBinaryExpression(node)) {
|
|
87
|
+
const op = node.getOperatorToken().getText();
|
|
88
|
+
// Assignment: a = b → handled at statement level, but can appear in expressions
|
|
89
|
+
if (op === "=") {
|
|
90
|
+
// This is an assignment expression; extract as binop for now
|
|
91
|
+
return { kind: "binop", op: "=", left: extractExpr(node.getLeft()), right: extractExpr(node.getRight()) };
|
|
92
|
+
}
|
|
93
|
+
return { kind: "binop", op, left: extractExpr(node.getLeft()), right: extractExpr(node.getRight()) };
|
|
94
|
+
}
|
|
95
|
+
// Prefix unary: !x, -x
|
|
96
|
+
if (Node.isPrefixUnaryExpression(node)) {
|
|
97
|
+
const opToken = node.getOperatorToken();
|
|
98
|
+
let op;
|
|
99
|
+
switch (opToken) {
|
|
100
|
+
case SyntaxKind.ExclamationToken:
|
|
101
|
+
op = "!";
|
|
102
|
+
break;
|
|
103
|
+
case SyntaxKind.MinusToken:
|
|
104
|
+
op = "-";
|
|
105
|
+
break;
|
|
106
|
+
case SyntaxKind.PlusToken:
|
|
107
|
+
op = "+";
|
|
108
|
+
break;
|
|
109
|
+
default: op = String(opToken);
|
|
110
|
+
}
|
|
111
|
+
return { kind: "unop", op, expr: extractExpr(node.getOperand()) };
|
|
112
|
+
}
|
|
113
|
+
// Parenthesized: (x)
|
|
114
|
+
if (Node.isParenthesizedExpression(node)) {
|
|
115
|
+
return extractExpr(node.getExpression());
|
|
116
|
+
}
|
|
117
|
+
// Arrow function: (x) => expr or (x) => { stmts }
|
|
118
|
+
if (Node.isArrowFunction(node)) {
|
|
119
|
+
const params = node.getParameters().map(p => {
|
|
120
|
+
const typeNode = p.getTypeNode();
|
|
121
|
+
return { name: p.getName(), tsType: typeNode ? typeNode.getText() : undefined };
|
|
122
|
+
});
|
|
123
|
+
const body = node.getBody();
|
|
124
|
+
if (Node.isExpression(body)) {
|
|
125
|
+
return { kind: "lambda", params, body: extractExpr(body) };
|
|
126
|
+
}
|
|
127
|
+
if (Node.isBlock(body)) {
|
|
128
|
+
return { kind: "lambda", params, body: extractStmts(body.getStatements()) };
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`Unsupported arrow function body: ${node.getText().slice(0, 80)}`);
|
|
131
|
+
}
|
|
132
|
+
// Array literal: [a, b, c] → arrayLiteral, [...arr, elem] → push(arr, elem)
|
|
133
|
+
if (Node.isArrayLiteralExpression(node)) {
|
|
134
|
+
const elems = node.getElements();
|
|
135
|
+
// [...arr, elem] → push(arr, elem)
|
|
136
|
+
if (elems.length === 2 && Node.isSpreadElement(elems[0])) {
|
|
137
|
+
return { kind: "call", fn: { kind: "field", obj: extractExpr(elems[0].getExpression()), field: "push" }, args: [extractExpr(elems[1])] };
|
|
138
|
+
}
|
|
139
|
+
// [a, b, c] or [] → arrayLiteral
|
|
140
|
+
return { kind: "arrayLiteral", elems: elems.map(e => extractExpr(e)) };
|
|
141
|
+
}
|
|
142
|
+
// Object literal: { res: true, done: false } or { ...obj, res: true }
|
|
143
|
+
if (Node.isObjectLiteralExpression(node)) {
|
|
144
|
+
let spread = null;
|
|
145
|
+
const fields = [];
|
|
146
|
+
for (const prop of node.getProperties()) {
|
|
147
|
+
if (Node.isSpreadAssignment(prop)) {
|
|
148
|
+
spread = extractExpr(prop.getExpression());
|
|
149
|
+
}
|
|
150
|
+
else if (Node.isShorthandPropertyAssignment(prop)) {
|
|
151
|
+
const name = prop.getName();
|
|
152
|
+
fields.push({ name, value: { kind: "var", name } });
|
|
153
|
+
}
|
|
154
|
+
else if (Node.isPropertyAssignment(prop)) {
|
|
155
|
+
const init = prop.getInitializer();
|
|
156
|
+
if (init)
|
|
157
|
+
fields.push({ name: prop.getName(), value: extractExpr(init) });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { kind: "record", spread, fields };
|
|
161
|
+
}
|
|
162
|
+
// Ternary: cond ? then : else
|
|
163
|
+
if (Node.isConditionalExpression(node)) {
|
|
164
|
+
return { kind: "conditional", cond: extractExpr(node.getCondition()), then: extractExpr(node.getWhenTrue()), else: extractExpr(node.getWhenFalse()) };
|
|
165
|
+
}
|
|
166
|
+
// Non-null assertion: expr!
|
|
167
|
+
if (Node.isNonNullExpression(node)) {
|
|
168
|
+
return { kind: "nonNull", expr: extractExpr(node.getExpression()) };
|
|
169
|
+
}
|
|
170
|
+
// new Map<K,V>() / new Set<T>() — with or without initializer
|
|
171
|
+
if (Node.isNewExpression(node)) {
|
|
172
|
+
const name = node.getExpression().getText();
|
|
173
|
+
if (name === "Map" || name === "Set") {
|
|
174
|
+
const typeArgs = node.getTypeArguments();
|
|
175
|
+
const tsType = typeArgs && typeArgs.length > 0
|
|
176
|
+
? `${name}<${typeArgs.map(t => t.getText()).join(", ")}>`
|
|
177
|
+
: name;
|
|
178
|
+
const args = node.getArguments();
|
|
179
|
+
// new Map(arr.map(fn)) — map-from-array constructor
|
|
180
|
+
if (name === "Map" && args && args.length === 1) {
|
|
181
|
+
return { kind: "call", fn: { kind: "var", name: "__mapFromArray" }, args: [extractExpr(args[0])] };
|
|
182
|
+
}
|
|
183
|
+
return { kind: "emptyCollection", collectionType: name, tsType };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// As-expression: expr as T — strip the type assertion
|
|
187
|
+
if (Node.isAsExpression(node)) {
|
|
188
|
+
return extractExpr(node.getExpression());
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`Unsupported expression: ${node.getText()}`);
|
|
191
|
+
}
|
|
192
|
+
// ── Annotation parsing ───────────────────────────────────────
|
|
193
|
+
const PREFIX = "//@ ";
|
|
194
|
+
const KEYWORDS = ["requires", "ensures", "invariant", "decreases", "done_with", "type"];
|
|
195
|
+
function parseAnnotations(node) {
|
|
196
|
+
const result = [];
|
|
197
|
+
for (const range of node.getLeadingCommentRanges()) {
|
|
198
|
+
const text = range.getText().trim();
|
|
199
|
+
if (!text.startsWith(PREFIX))
|
|
200
|
+
continue;
|
|
201
|
+
const content = text.slice(PREFIX.length);
|
|
202
|
+
const sp = content.indexOf(" ");
|
|
203
|
+
if (sp === -1)
|
|
204
|
+
continue;
|
|
205
|
+
const kw = content.slice(0, sp);
|
|
206
|
+
if (!KEYWORDS.includes(kw))
|
|
207
|
+
continue;
|
|
208
|
+
result.push({ kind: kw, expr: content.slice(sp + 1).trim() });
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
function collectAnnotations(node, body) {
|
|
213
|
+
const own = parseAnnotations(node);
|
|
214
|
+
if (body && body.length > 0)
|
|
215
|
+
return [...own, ...parseAnnotations(body[0])];
|
|
216
|
+
return own;
|
|
217
|
+
}
|
|
218
|
+
// ── Type declaration extraction ──────────────────────────────
|
|
219
|
+
function extractTypeDecl(decl) {
|
|
220
|
+
const name = decl.getName();
|
|
221
|
+
const type = decl.getType();
|
|
222
|
+
if (type.isUnion()) {
|
|
223
|
+
const members = type.getUnionTypes();
|
|
224
|
+
if (members.every(m => m.isStringLiteral())) {
|
|
225
|
+
return { name, kind: "string-union", values: members.map(m => m.getLiteralValue()) };
|
|
226
|
+
}
|
|
227
|
+
if (members.every(m => m.isObject())) {
|
|
228
|
+
const discriminant = findDiscriminant(members);
|
|
229
|
+
if (discriminant) {
|
|
230
|
+
const variants = members.map(m => {
|
|
231
|
+
const tagProp = m.getProperty(discriminant);
|
|
232
|
+
const tagType = tagProp?.getTypeAtLocation(decl);
|
|
233
|
+
const tag = tagType?.getLiteralValue();
|
|
234
|
+
const fields = [];
|
|
235
|
+
for (const prop of m.getProperties()) {
|
|
236
|
+
if (prop.getName() === discriminant)
|
|
237
|
+
continue;
|
|
238
|
+
fields.push({ name: prop.getName(), tsType: typeToString(prop.getTypeAtLocation(decl)) });
|
|
239
|
+
}
|
|
240
|
+
return { name: tag, fields };
|
|
241
|
+
});
|
|
242
|
+
return { name, kind: "discriminated-union", discriminant, variants };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (type.isObject())
|
|
247
|
+
return extractRecord(name, type, decl);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
function extractInterface(decl) {
|
|
251
|
+
// Collect field type overrides from trailing //@ type annotations
|
|
252
|
+
const overrides = new Map();
|
|
253
|
+
for (const member of decl.getMembers()) {
|
|
254
|
+
if (Node.isPropertySignature(member)) {
|
|
255
|
+
const text = member.getTrailingCommentRanges().map(r => r.getText()).join(" ");
|
|
256
|
+
const match = text.match(/\/\/@ type (\w+)/);
|
|
257
|
+
if (match)
|
|
258
|
+
overrides.set(member.getName(), match[1]);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return extractRecord(decl.getName(), decl.getType(), decl, overrides);
|
|
262
|
+
}
|
|
263
|
+
function extractRecord(name, type, locationNode, overrides) {
|
|
264
|
+
const props = type.getProperties();
|
|
265
|
+
if (props.length === 0)
|
|
266
|
+
return null;
|
|
267
|
+
const fields = [];
|
|
268
|
+
for (const prop of props) {
|
|
269
|
+
const override = overrides?.get(prop.getName());
|
|
270
|
+
const tsType = override ?? typeToString(prop.getTypeAtLocation(locationNode));
|
|
271
|
+
fields.push({ name: prop.getName(), tsType });
|
|
272
|
+
}
|
|
273
|
+
return { name, kind: "record", fields };
|
|
274
|
+
}
|
|
275
|
+
function findDiscriminant(members) {
|
|
276
|
+
if (members.length === 0)
|
|
277
|
+
return null;
|
|
278
|
+
const firstProps = members[0].getProperties();
|
|
279
|
+
for (const prop of firstProps) {
|
|
280
|
+
const name = prop.getName();
|
|
281
|
+
const allHave = members.every(m => {
|
|
282
|
+
const p = m.getProperty(name);
|
|
283
|
+
if (!p)
|
|
284
|
+
return false;
|
|
285
|
+
const t = p.getDeclarations()[0] ? p.getTypeAtLocation(p.getDeclarations()[0]) : null;
|
|
286
|
+
return t?.isStringLiteral() ?? false;
|
|
287
|
+
});
|
|
288
|
+
if (allHave)
|
|
289
|
+
return name;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function typeToString(type) {
|
|
294
|
+
if (type.isUndefined())
|
|
295
|
+
return "undefined";
|
|
296
|
+
if (type.isNumber() || type.isNumberLiteral())
|
|
297
|
+
return "number";
|
|
298
|
+
if (type.isBigInt() || type.isBigIntLiteral())
|
|
299
|
+
return "bigint";
|
|
300
|
+
if (type.isString() || type.isStringLiteral())
|
|
301
|
+
return "string";
|
|
302
|
+
if (type.isBoolean())
|
|
303
|
+
return "boolean";
|
|
304
|
+
// Named type alias (e.g. Priority = "low" | "medium" | "high") — use the alias name
|
|
305
|
+
if (type.getAliasSymbol()) {
|
|
306
|
+
const name = type.getAliasSymbol().getName();
|
|
307
|
+
const args = type.getAliasTypeArguments();
|
|
308
|
+
if (args.length > 0)
|
|
309
|
+
return `${name}<${args.map(t => typeToString(t)).join(", ")}>`;
|
|
310
|
+
return name;
|
|
311
|
+
}
|
|
312
|
+
if (type.isUnion()) {
|
|
313
|
+
return type.getUnionTypes().map(typeToString).join(" | ");
|
|
314
|
+
}
|
|
315
|
+
if (type.isArray()) {
|
|
316
|
+
const elem = type.getArrayElementTypeOrThrow();
|
|
317
|
+
return `${typeToString(elem)}[]`;
|
|
318
|
+
}
|
|
319
|
+
const symbol = type.getSymbol() ?? type.getAliasSymbol();
|
|
320
|
+
if (symbol) {
|
|
321
|
+
const name = symbol.getName();
|
|
322
|
+
const typeArgs = type.getTypeArguments();
|
|
323
|
+
if (typeArgs.length > 0) {
|
|
324
|
+
return `${name}<${typeArgs.map(t => typeToString(t)).join(", ")}>`;
|
|
325
|
+
}
|
|
326
|
+
return name;
|
|
327
|
+
}
|
|
328
|
+
return type.getText();
|
|
329
|
+
}
|
|
330
|
+
const COMPOUND_OPS = {
|
|
331
|
+
"+=": "+", "-=": "-", "*=": "*", "/=": "/", "%=": "%",
|
|
332
|
+
};
|
|
333
|
+
// ── Statement extraction ─────────────────────────────────────
|
|
334
|
+
/** Parse ghost and assert annotations from comment ranges. */
|
|
335
|
+
function parseSpecComments(ranges, line) {
|
|
336
|
+
const result = [];
|
|
337
|
+
for (const range of ranges) {
|
|
338
|
+
const text = range.getText().trim();
|
|
339
|
+
if (!text.startsWith(PREFIX))
|
|
340
|
+
continue;
|
|
341
|
+
const content = text.slice(PREFIX.length);
|
|
342
|
+
// assert expr
|
|
343
|
+
if (content.startsWith("assert ")) {
|
|
344
|
+
result.push({ kind: "assert", expr: content.slice(7).trim(), line });
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!content.startsWith("ghost "))
|
|
348
|
+
continue;
|
|
349
|
+
const ghostBody = content.slice(6).trim();
|
|
350
|
+
// ghost let varName: type = expr OR ghost let varName = expr
|
|
351
|
+
const letMatch = ghostBody.match(/^let\s+(\w+)(?:\s*:\s*(\w+))?\s*=\s*(.+)$/);
|
|
352
|
+
if (letMatch) {
|
|
353
|
+
result.push({ kind: "ghostLet", name: letMatch[1], tsType: letMatch[2] ?? null, init: letMatch[3].trim(), line });
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// ghost varName = expr
|
|
357
|
+
const assignMatch = ghostBody.match(/^(\w+)\s*=\s*(.+)$/);
|
|
358
|
+
if (assignMatch) {
|
|
359
|
+
result.push({ kind: "ghostAssign", target: assignMatch[1], value: assignMatch[2].trim(), line });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
function extractStmts(stmts) {
|
|
365
|
+
const result = [];
|
|
366
|
+
for (const s of stmts) {
|
|
367
|
+
const line = s.getStartLineNumber();
|
|
368
|
+
// Ghost annotations from leading comments → inject before this statement
|
|
369
|
+
result.push(...parseSpecComments(s.getLeadingCommentRanges(), line));
|
|
370
|
+
if (Node.isVariableStatement(s)) {
|
|
371
|
+
const havocMatch = s.getLeadingCommentRanges()
|
|
372
|
+
.map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s+(\S+))?$/))
|
|
373
|
+
.find(m => m !== null);
|
|
374
|
+
const havocKey = havocMatch?.[1] ?? null;
|
|
375
|
+
const isHavoc = !!havocMatch;
|
|
376
|
+
for (const d of s.getDeclarations()) {
|
|
377
|
+
const declType = d.getType();
|
|
378
|
+
let init;
|
|
379
|
+
if (isHavoc && !havocKey) {
|
|
380
|
+
init = { kind: "havoc", tsType: typeToString(declType) };
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const initializer = d.getInitializer();
|
|
384
|
+
_havocKey = havocKey;
|
|
385
|
+
init = initializer ? extractExpr(initializer) : { kind: "var", name: "default" };
|
|
386
|
+
_havocKey = null;
|
|
387
|
+
}
|
|
388
|
+
result.push({
|
|
389
|
+
kind: "let",
|
|
390
|
+
name: d.getName(),
|
|
391
|
+
mutable: s.getDeclarationKind() === "let",
|
|
392
|
+
tsType: typeToString(declType),
|
|
393
|
+
init,
|
|
394
|
+
line,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (Node.isWhileStatement(s)) {
|
|
400
|
+
const bodyNode = s.getStatement();
|
|
401
|
+
const bodyStmts = Node.isBlock(bodyNode) ? bodyNode.getStatements() : [];
|
|
402
|
+
const annots = collectAnnotations(s, bodyStmts);
|
|
403
|
+
result.push({
|
|
404
|
+
kind: "while",
|
|
405
|
+
cond: extractExpr(s.getExpression()),
|
|
406
|
+
invariants: annots.filter(a => a.kind === "invariant").map(a => a.expr),
|
|
407
|
+
decreases: annots.find(a => a.kind === "decreases")?.expr ?? null,
|
|
408
|
+
doneWith: annots.find(a => a.kind === "done_with")?.expr ?? null,
|
|
409
|
+
body: extractStmts(bodyStmts),
|
|
410
|
+
line,
|
|
411
|
+
});
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (Node.isForOfStatement(s)) {
|
|
415
|
+
const init = s.getInitializer();
|
|
416
|
+
const names = [];
|
|
417
|
+
if (Node.isVariableDeclarationList(init)) {
|
|
418
|
+
const decl = init.getDeclarations()[0];
|
|
419
|
+
const nameNode = decl?.getNameNode();
|
|
420
|
+
if (nameNode && Node.isArrayBindingPattern(nameNode)) {
|
|
421
|
+
for (const elem of nameNode.getElements()) {
|
|
422
|
+
if (Node.isBindingElement(elem))
|
|
423
|
+
names.push(elem.getNameNode().getText());
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
names.push(decl?.getName() ?? "_");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
names.push("_");
|
|
432
|
+
}
|
|
433
|
+
const bodyNode = s.getStatement();
|
|
434
|
+
const bodyStmts = Node.isBlock(bodyNode) ? bodyNode.getStatements() : [bodyNode];
|
|
435
|
+
const annots = collectAnnotations(s, bodyStmts);
|
|
436
|
+
result.push({
|
|
437
|
+
kind: "forof",
|
|
438
|
+
names,
|
|
439
|
+
iterable: extractExpr(s.getExpression()),
|
|
440
|
+
invariants: annots.filter(a => a.kind === "invariant").map(a => a.expr),
|
|
441
|
+
doneWith: annots.find(a => a.kind === "done_with")?.expr ?? null,
|
|
442
|
+
body: extractStmts(bodyStmts),
|
|
443
|
+
line,
|
|
444
|
+
});
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (Node.isIfStatement(s)) {
|
|
448
|
+
const thenNode = s.getThenStatement();
|
|
449
|
+
const elseNode = s.getElseStatement();
|
|
450
|
+
result.push({
|
|
451
|
+
kind: "if",
|
|
452
|
+
cond: extractExpr(s.getExpression()),
|
|
453
|
+
then: Node.isBlock(thenNode) ? extractStmts(thenNode.getStatements()) : extractStmts([thenNode]),
|
|
454
|
+
else: elseNode
|
|
455
|
+
? Node.isBlock(elseNode) ? extractStmts(elseNode.getStatements()) : extractStmts([elseNode])
|
|
456
|
+
: [],
|
|
457
|
+
line,
|
|
458
|
+
});
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (Node.isSwitchStatement(s)) {
|
|
462
|
+
const exprNode = s.getExpression();
|
|
463
|
+
const exprAst = extractExpr(exprNode);
|
|
464
|
+
const discriminant = exprAst.kind === "field" ? exprAst.field : "";
|
|
465
|
+
const switchExpr = exprAst.kind === "field" ? exprAst.obj : exprAst;
|
|
466
|
+
const cases = [];
|
|
467
|
+
let defaultBody = [];
|
|
468
|
+
for (const clause of s.getClauses()) {
|
|
469
|
+
if (Node.isCaseClause(clause)) {
|
|
470
|
+
const label = clause.getExpression().getText().replace(/^["']|["']$/g, "");
|
|
471
|
+
const bodyStmts = clause.getStatements().filter(st => !Node.isBreakStatement(st));
|
|
472
|
+
cases.push({ label, body: extractStmts(bodyStmts) });
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
const bodyStmts = clause.getStatements().filter(st => !Node.isBreakStatement(st));
|
|
476
|
+
defaultBody = extractStmts(bodyStmts);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
result.push({ kind: "switch", expr: switchExpr, discriminant, cases, defaultBody, line });
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (Node.isReturnStatement(s)) {
|
|
483
|
+
const expr = s.getExpression();
|
|
484
|
+
result.push({ kind: "return", value: expr ? extractExpr(expr) : { kind: "var", name: "()" }, line });
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (Node.isBreakStatement(s)) {
|
|
488
|
+
result.push({ kind: "break", line });
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (Node.isContinueStatement(s)) {
|
|
492
|
+
result.push({ kind: "continue", line });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (Node.isExpressionStatement(s)) {
|
|
496
|
+
const expr = s.getExpression();
|
|
497
|
+
// arr[i] = v → arr = arr.with(i, v)
|
|
498
|
+
if (Node.isBinaryExpression(expr) && expr.getOperatorToken().getText() === "=" && Node.isElementAccessExpression(expr.getLeft())) {
|
|
499
|
+
const left = expr.getLeft();
|
|
500
|
+
const obj = extractExpr(left.getExpression());
|
|
501
|
+
const idx = extractExpr(left.getArgumentExpression());
|
|
502
|
+
const val = extractExpr(expr.getRight());
|
|
503
|
+
const target = left.getExpression().getText();
|
|
504
|
+
const withCall = { kind: "call", fn: { kind: "field", obj, field: "with" }, args: [idx, val] };
|
|
505
|
+
result.push({ kind: "assign", target, value: withCall, line });
|
|
506
|
+
// x = e
|
|
507
|
+
}
|
|
508
|
+
else if (Node.isBinaryExpression(expr) && expr.getOperatorToken().getText() === "=") {
|
|
509
|
+
result.push({ kind: "assign", target: expr.getLeft().getText(), value: extractExpr(expr.getRight()), line });
|
|
510
|
+
// x += e, x -= e, etc.
|
|
511
|
+
}
|
|
512
|
+
else if (Node.isBinaryExpression(expr) && COMPOUND_OPS[expr.getOperatorToken().getText()]) {
|
|
513
|
+
const op = COMPOUND_OPS[expr.getOperatorToken().getText()];
|
|
514
|
+
const target = expr.getLeft().getText();
|
|
515
|
+
result.push({ kind: "assign", target, value: { kind: "binop", op, left: { kind: "var", name: target }, right: extractExpr(expr.getRight()) }, line });
|
|
516
|
+
// i++, i--
|
|
517
|
+
}
|
|
518
|
+
else if (Node.isPostfixUnaryExpression(expr)) {
|
|
519
|
+
const target = expr.getOperand().getText();
|
|
520
|
+
const op = expr.getOperatorToken() === SyntaxKind.PlusPlusToken ? "+" : "-";
|
|
521
|
+
result.push({ kind: "assign", target, value: { kind: "binop", op, left: { kind: "var", name: target }, right: { kind: "num", value: 1 } }, line });
|
|
522
|
+
// ++i, --i
|
|
523
|
+
}
|
|
524
|
+
else if (Node.isPrefixUnaryExpression(expr) && (expr.getOperatorToken() === SyntaxKind.PlusPlusToken || expr.getOperatorToken() === SyntaxKind.MinusMinusToken)) {
|
|
525
|
+
const target = expr.getOperand().getText();
|
|
526
|
+
const op = expr.getOperatorToken() === SyntaxKind.PlusPlusToken ? "+" : "-";
|
|
527
|
+
result.push({ kind: "assign", target, value: { kind: "binop", op, left: { kind: "var", name: target }, right: { kind: "num", value: 1 } }, line });
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
result.push({ kind: "expr", expr: extractExpr(expr), line });
|
|
531
|
+
}
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (Node.isThrowStatement(s)) {
|
|
535
|
+
result.push({ kind: "throw", line });
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
throw new Error(`Unsupported statement at line ${line}: ${s.getText().slice(0, 80)}`);
|
|
539
|
+
}
|
|
540
|
+
// Ghost comments after the last statement (before closing brace) appear as sibling trivia nodes
|
|
541
|
+
if (stmts.length > 0) {
|
|
542
|
+
const last = stmts[stmts.length - 1];
|
|
543
|
+
const line = last.getStartLineNumber();
|
|
544
|
+
for (const sib of last.getNextSiblings()) {
|
|
545
|
+
const text = sib.getText().trim();
|
|
546
|
+
if (!text.startsWith(PREFIX))
|
|
547
|
+
continue;
|
|
548
|
+
const content = text.slice(PREFIX.length);
|
|
549
|
+
// assert expr
|
|
550
|
+
if (content.startsWith("assert ")) {
|
|
551
|
+
result.push({ kind: "assert", expr: content.slice(7).trim(), line });
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (!content.startsWith("ghost "))
|
|
555
|
+
continue;
|
|
556
|
+
const ghostBody = content.slice(6).trim();
|
|
557
|
+
const letMatch = ghostBody.match(/^let\s+(\w+)(?:\s*:\s*(\w+))?\s*=\s*(.+)$/);
|
|
558
|
+
if (letMatch) {
|
|
559
|
+
result.push({ kind: "ghostLet", name: letMatch[1], tsType: letMatch[2] ?? null, init: letMatch[3].trim(), line });
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const assignMatch = ghostBody.match(/^(\w+)\s*=\s*(.+)$/);
|
|
563
|
+
if (assignMatch) {
|
|
564
|
+
result.push({ kind: "ghostAssign", target: assignMatch[1], value: assignMatch[2].trim(), line });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
// ── Function extraction ──────────────────────────────────────
|
|
571
|
+
function extractFunction(fn) {
|
|
572
|
+
const body = fn.getBody();
|
|
573
|
+
if (!body || !Node.isBlock(body))
|
|
574
|
+
throw new Error(`${fn.getName?.() ?? "arrow"}: function body is not a block`);
|
|
575
|
+
const bodyStmts = body.getStatements();
|
|
576
|
+
const annots = collectAnnotations(fn, bodyStmts);
|
|
577
|
+
const typeAnnotations = [];
|
|
578
|
+
for (const a of annots) {
|
|
579
|
+
if (a.kind === "type") {
|
|
580
|
+
const parts = a.expr.split(/\s+/);
|
|
581
|
+
if (parts.length === 2)
|
|
582
|
+
typeAnnotations.push({ name: parts[0], type: parts[1] });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
name: fn.getName?.() ?? "<anonymous>",
|
|
587
|
+
params: fn.getParameters().map(p => ({ name: p.getName(), tsType: p.getTypeNode()?.getText() ?? "unknown" })),
|
|
588
|
+
returnType: fn.getReturnTypeNode()?.getText() ?? "unknown",
|
|
589
|
+
requires: annots.filter(a => a.kind === "requires").map(a => a.expr),
|
|
590
|
+
ensures: annots.filter(a => a.kind === "ensures").map(a => a.expr),
|
|
591
|
+
typeAnnotations,
|
|
592
|
+
body: extractStmts(bodyStmts),
|
|
593
|
+
line: fn.getStartLineNumber(),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
// ── Module extraction ────────────────────────────────────────
|
|
597
|
+
export function extractModule(sourceFile) {
|
|
598
|
+
const typeDecls = [];
|
|
599
|
+
// Extract type declarations in source order to respect dependencies
|
|
600
|
+
for (const stmt of sourceFile.getStatements()) {
|
|
601
|
+
if (Node.isTypeAliasDeclaration(stmt)) {
|
|
602
|
+
const info = extractTypeDecl(stmt);
|
|
603
|
+
if (info)
|
|
604
|
+
typeDecls.push(info);
|
|
605
|
+
}
|
|
606
|
+
else if (Node.isInterfaceDeclaration(stmt)) {
|
|
607
|
+
const info = extractInterface(stmt);
|
|
608
|
+
if (info)
|
|
609
|
+
typeDecls.push(info);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Extract module-level const declarations
|
|
613
|
+
const constants = [];
|
|
614
|
+
for (const stmt of sourceFile.getStatements()) {
|
|
615
|
+
if (Node.isVariableStatement(stmt)) {
|
|
616
|
+
for (const decl of stmt.getDeclarationList().getDeclarations()) {
|
|
617
|
+
if (stmt.getDeclarationList().getFlags() & 2 /* const */) {
|
|
618
|
+
const init = decl.getInitializer();
|
|
619
|
+
// Skip huge string constants — they crash the verifier and have no verification value
|
|
620
|
+
const initType = decl.getType();
|
|
621
|
+
const isHugeString = (initType.isString() || initType.isStringLiteral()) && init.getText().length > 200;
|
|
622
|
+
if (init && !isHugeString && !Node.isArrowFunction(init)) {
|
|
623
|
+
try {
|
|
624
|
+
constants.push({
|
|
625
|
+
name: decl.getName(),
|
|
626
|
+
tsType: typeToString(decl.getType()),
|
|
627
|
+
value: extractExpr(init),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
catch (e) {
|
|
631
|
+
console.error(`WARNING: skipping const '${decl.getName()}': ${e.message}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Collect all function-like declarations: function declarations + const arrow functions
|
|
639
|
+
const allFns = [];
|
|
640
|
+
for (const fn of sourceFile.getFunctions()) {
|
|
641
|
+
allFns.push({ name: fn.getName() ?? "<anonymous>", node: fn });
|
|
642
|
+
}
|
|
643
|
+
// const f = (...) => { ... } — treat as named function
|
|
644
|
+
for (const stmt of sourceFile.getStatements()) {
|
|
645
|
+
if (Node.isVariableStatement(stmt)) {
|
|
646
|
+
for (const decl of stmt.getDeclarationList().getDeclarations()) {
|
|
647
|
+
const init = decl.getInitializer();
|
|
648
|
+
if (init && Node.isArrowFunction(init)) {
|
|
649
|
+
allFns.push({ name: decl.getName(), node: init });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// If any function has //@ verify, only extract those (brownfield mode).
|
|
655
|
+
// Otherwise extract all functions (backwards-compatible with existing examples).
|
|
656
|
+
const hasVerifyDirective = allFns.some(f => f.node.getFullText().includes('//@ verify'));
|
|
657
|
+
const fnsToExtract = hasVerifyDirective
|
|
658
|
+
? allFns.filter(f => f.node.getFullText().includes('//@ verify'))
|
|
659
|
+
: allFns;
|
|
660
|
+
const functions = fnsToExtract.map(f => {
|
|
661
|
+
const raw = extractFunction(f.node);
|
|
662
|
+
raw.name = f.name; // use the const name, not "<anonymous>"
|
|
663
|
+
return raw;
|
|
664
|
+
});
|
|
665
|
+
// In brownfield mode, filter consts to only those referenced by verified functions.
|
|
666
|
+
// Types are NOT filtered — they may be needed transitively (e.g. Option from T | undefined).
|
|
667
|
+
if (hasVerifyDirective) {
|
|
668
|
+
const referencedNames = new Set();
|
|
669
|
+
function collectNames(stmts) {
|
|
670
|
+
for (const s of stmts) {
|
|
671
|
+
if (s.kind === "let") {
|
|
672
|
+
collectNamesExpr(s.init);
|
|
673
|
+
}
|
|
674
|
+
if (s.kind === "assign") {
|
|
675
|
+
collectNamesExpr(s.value);
|
|
676
|
+
}
|
|
677
|
+
if (s.kind === "return") {
|
|
678
|
+
collectNamesExpr(s.value);
|
|
679
|
+
}
|
|
680
|
+
if (s.kind === "if") {
|
|
681
|
+
collectNamesExpr(s.cond);
|
|
682
|
+
collectNames(s.then);
|
|
683
|
+
collectNames(s.else);
|
|
684
|
+
}
|
|
685
|
+
if (s.kind === "while") {
|
|
686
|
+
collectNamesExpr(s.cond);
|
|
687
|
+
collectNames(s.body);
|
|
688
|
+
}
|
|
689
|
+
if (s.kind === "forof") {
|
|
690
|
+
collectNamesExpr(s.iterable);
|
|
691
|
+
collectNames(s.body);
|
|
692
|
+
}
|
|
693
|
+
if (s.kind === "expr") {
|
|
694
|
+
collectNamesExpr(s.expr);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function collectNamesExpr(e) {
|
|
699
|
+
if (e.kind === "var")
|
|
700
|
+
referencedNames.add(e.name);
|
|
701
|
+
if (e.kind === "binop") {
|
|
702
|
+
collectNamesExpr(e.left);
|
|
703
|
+
collectNamesExpr(e.right);
|
|
704
|
+
}
|
|
705
|
+
if (e.kind === "unop") {
|
|
706
|
+
collectNamesExpr(e.expr);
|
|
707
|
+
}
|
|
708
|
+
if (e.kind === "call") {
|
|
709
|
+
collectNamesExpr(e.fn);
|
|
710
|
+
e.args.forEach(collectNamesExpr);
|
|
711
|
+
}
|
|
712
|
+
if (e.kind === "field") {
|
|
713
|
+
collectNamesExpr(e.obj);
|
|
714
|
+
}
|
|
715
|
+
if (e.kind === "index") {
|
|
716
|
+
collectNamesExpr(e.obj);
|
|
717
|
+
collectNamesExpr(e.idx);
|
|
718
|
+
}
|
|
719
|
+
if (e.kind === "record") {
|
|
720
|
+
if (e.spread)
|
|
721
|
+
collectNamesExpr(e.spread);
|
|
722
|
+
e.fields.forEach(f => collectNamesExpr(f.value));
|
|
723
|
+
}
|
|
724
|
+
if (e.kind === "arrayLiteral") {
|
|
725
|
+
e.elems.forEach(collectNamesExpr);
|
|
726
|
+
}
|
|
727
|
+
if (e.kind === "conditional") {
|
|
728
|
+
collectNamesExpr(e.cond);
|
|
729
|
+
collectNamesExpr(e.then);
|
|
730
|
+
collectNamesExpr(e.else);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
for (const fn of functions) {
|
|
734
|
+
for (const p of fn.params)
|
|
735
|
+
referencedNames.add(p.tsType);
|
|
736
|
+
referencedNames.add(fn.returnType);
|
|
737
|
+
collectNames(fn.body);
|
|
738
|
+
// Also scan spec annotations for identifier references
|
|
739
|
+
for (const spec of [...fn.requires, ...fn.ensures]) {
|
|
740
|
+
for (const m of spec.matchAll(/\b([a-zA-Z_]\w*)\b/g)) {
|
|
741
|
+
referencedNames.add(m[1]);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
constants.splice(0, constants.length, ...constants.filter(c => referencedNames.has(c.name)));
|
|
746
|
+
}
|
|
747
|
+
// Resolve imported types: extract types referenced in function signatures but not in this file
|
|
748
|
+
const knownTypes = new Set(typeDecls.map(d => d.name));
|
|
749
|
+
const builtins = new Set(["Map", "Set", "Array", "String", "Number", "Boolean", "Promise", "Date", "RegExp", "Error"]);
|
|
750
|
+
function resolveType(t, locationNode) {
|
|
751
|
+
// Unwrap arrays and generics to find user-defined types
|
|
752
|
+
if (t.isArray()) {
|
|
753
|
+
resolveType(t.getArrayElementTypeOrThrow(), locationNode);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
for (const arg of t.getTypeArguments())
|
|
757
|
+
resolveType(arg, locationNode);
|
|
758
|
+
const sym = t.getSymbol() ?? t.getAliasSymbol();
|
|
759
|
+
const name = sym?.getName();
|
|
760
|
+
if (name && !knownTypes.has(name) && !builtins.has(name) && t.isObject()) {
|
|
761
|
+
const info = extractRecord(name, t, locationNode);
|
|
762
|
+
if (info) {
|
|
763
|
+
typeDecls.push(info);
|
|
764
|
+
knownTypes.add(name);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
for (const f of fnsToExtract) {
|
|
769
|
+
for (const p of f.node.getParameters())
|
|
770
|
+
resolveType(p.getType(), p);
|
|
771
|
+
}
|
|
772
|
+
// Extract classes with //@ verify methods
|
|
773
|
+
const classes = [];
|
|
774
|
+
for (const cls of sourceFile.getClasses()) {
|
|
775
|
+
const methods = [];
|
|
776
|
+
for (const method of cls.getMethods()) {
|
|
777
|
+
if (!method.getFullText().includes('//@ verify'))
|
|
778
|
+
continue;
|
|
779
|
+
methods.push(extractFunction(method));
|
|
780
|
+
}
|
|
781
|
+
if (methods.length === 0)
|
|
782
|
+
continue;
|
|
783
|
+
const fields = [];
|
|
784
|
+
for (const prop of cls.getProperties()) {
|
|
785
|
+
fields.push({ name: prop.getName(), tsType: typeToString(prop.getType()) });
|
|
786
|
+
}
|
|
787
|
+
classes.push({ name: cls.getName() ?? "Anonymous", fields, methods });
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
file: sourceFile.getFilePath(),
|
|
791
|
+
typeDecls,
|
|
792
|
+
constants,
|
|
793
|
+
functions,
|
|
794
|
+
classes,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
// ── Main ─────────────────────────────────────────────────────
|
|
798
|
+
if (process.argv[1]?.match(/extract\.(ts|js)$/)) {
|
|
799
|
+
const file = process.argv[2];
|
|
800
|
+
if (!file) {
|
|
801
|
+
console.error("Usage: extract <file.ts>");
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
804
|
+
const proj = new Project({ compilerOptions: { strict: true, target: ScriptTarget.ESNext, lib: ["lib.esnext.d.ts"] } });
|
|
805
|
+
console.log(JSON.stringify(extractModule(proj.addSourceFileAtPath(file)), null, 2));
|
|
806
|
+
}
|