lemmascript 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -31
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +47 -53
- package/tools/dist/dafny-emit.js +495 -93
- package/tools/dist/extract.js +847 -46
- package/tools/dist/ir.js +2 -2
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +397 -0
- package/tools/dist/lsc.js +62 -44
- package/tools/dist/resolve.js +500 -34
- package/tools/dist/specparser.js +66 -9
- package/tools/dist/transform.js +813 -202
- package/tools/dist/types.js +59 -13
package/tools/dist/extract.js
CHANGED
|
@@ -4,13 +4,63 @@
|
|
|
4
4
|
* Produces structured AST nodes, not strings.
|
|
5
5
|
* The only strings are //@ annotation expressions (parsed later by specparser).
|
|
6
6
|
*/
|
|
7
|
-
import { Project, Node, SyntaxKind } from "ts-morph";
|
|
7
|
+
import { Project, Node, SyntaxKind, ScriptTarget } from "ts-morph";
|
|
8
8
|
// ── Expression extraction ────────────────────────────────────
|
|
9
|
+
/** When set, calls whose function/method name matches this key are replaced with havoc. */
|
|
10
|
+
let _havocKey = null;
|
|
11
|
+
/** Generic bounds erasure map — set during extractFunction, applied in extractStmts. */
|
|
12
|
+
let _typeParamMap = new Map();
|
|
13
|
+
function _eraseGenerics(tsType) {
|
|
14
|
+
if (_typeParamMap.size === 0)
|
|
15
|
+
return tsType;
|
|
16
|
+
if (tsType.includes(" | ")) {
|
|
17
|
+
const arms = [...new Set(tsType.split(" | ").map(a => _eraseGenerics(a.trim())))];
|
|
18
|
+
return arms.length === 1 ? arms[0] : arms.join(" | ");
|
|
19
|
+
}
|
|
20
|
+
if (tsType.endsWith("[]"))
|
|
21
|
+
return _eraseGenerics(tsType.slice(0, -2)) + "[]";
|
|
22
|
+
if (_typeParamMap.has(tsType))
|
|
23
|
+
return _typeParamMap.get(tsType);
|
|
24
|
+
return tsType;
|
|
25
|
+
}
|
|
9
26
|
function extractExpr(node) {
|
|
27
|
+
// Havoc key matching: replace matching calls with havoc expression
|
|
28
|
+
if (_havocKey && Node.isCallExpression(node)) {
|
|
29
|
+
const fnExpr = node.getExpression();
|
|
30
|
+
const name = Node.isPropertyAccessExpression(fnExpr) ? fnExpr.getName()
|
|
31
|
+
: Node.isIdentifier(fnExpr) ? fnExpr.getText()
|
|
32
|
+
: null;
|
|
33
|
+
if (name === _havocKey) {
|
|
34
|
+
return { kind: "havoc", tsType: typeToString(node.getType()) };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
10
37
|
// Numeric literal
|
|
11
38
|
if (Node.isNumericLiteral(node)) {
|
|
12
39
|
return { kind: "num", value: Number(node.getLiteralValue()) };
|
|
13
40
|
}
|
|
41
|
+
// BigInt literal (e.g. 32n, 0xffffn) — treat as integer
|
|
42
|
+
if (Node.isBigIntLiteral(node)) {
|
|
43
|
+
const text = node.getText().replace(/n$/, '');
|
|
44
|
+
return { kind: "num", value: Number(text) };
|
|
45
|
+
}
|
|
46
|
+
// Template literal: `foo${x}bar` → "foo" + x + "bar"
|
|
47
|
+
if (Node.isTemplateExpression(node)) {
|
|
48
|
+
const parts = [];
|
|
49
|
+
const head = node.getHead().getLiteralText();
|
|
50
|
+
if (head)
|
|
51
|
+
parts.push({ kind: "str", value: head });
|
|
52
|
+
for (const span of node.getTemplateSpans()) {
|
|
53
|
+
parts.push(extractExpr(span.getExpression()));
|
|
54
|
+
const text = span.getLiteral().getLiteralText();
|
|
55
|
+
if (text)
|
|
56
|
+
parts.push({ kind: "str", value: text });
|
|
57
|
+
}
|
|
58
|
+
return parts.reduce((left, right) => ({ kind: "binop", op: "+", left, right }));
|
|
59
|
+
}
|
|
60
|
+
// No-substitution template literal: `hello` → "hello"
|
|
61
|
+
if (Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
62
|
+
return { kind: "str", value: node.getLiteralText() };
|
|
63
|
+
}
|
|
14
64
|
// String literal
|
|
15
65
|
if (Node.isStringLiteral(node)) {
|
|
16
66
|
return { kind: "str", value: node.getLiteralValue() };
|
|
@@ -24,9 +74,28 @@ function extractExpr(node) {
|
|
|
24
74
|
if (Node.isIdentifier(node)) {
|
|
25
75
|
return { kind: "var", name: node.getText() };
|
|
26
76
|
}
|
|
27
|
-
//
|
|
77
|
+
// this
|
|
78
|
+
if (node.getKind() === SyntaxKind.ThisKeyword) {
|
|
79
|
+
return { kind: "var", name: "this" };
|
|
80
|
+
}
|
|
81
|
+
// Property access: x.foo or x?.foo
|
|
28
82
|
if (Node.isPropertyAccessExpression(node)) {
|
|
29
|
-
|
|
83
|
+
const obj = extractExpr(node.getExpression());
|
|
84
|
+
const field = node.getName();
|
|
85
|
+
// Optional chaining on data access: x?.foo → x !== undefined ? x.foo : undefined
|
|
86
|
+
// Skip if this is a method call (parent is CallExpression) — handled differently.
|
|
87
|
+
if (node.hasQuestionDotToken()) {
|
|
88
|
+
const parent = node.getParent();
|
|
89
|
+
const isMethodCall = parent && Node.isCallExpression(parent) && parent.getExpression() === node;
|
|
90
|
+
if (!isMethodCall) {
|
|
91
|
+
return { kind: "conditional",
|
|
92
|
+
cond: { kind: "binop", op: "!==", left: obj, right: { kind: "var", name: "undefined" } },
|
|
93
|
+
then: { kind: "field", obj, field },
|
|
94
|
+
else: { kind: "var", name: "undefined" },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { kind: "field", obj, field };
|
|
30
99
|
}
|
|
31
100
|
// Element access: arr[i]
|
|
32
101
|
if (Node.isElementAccessExpression(node)) {
|
|
@@ -90,15 +159,38 @@ function extractExpr(node) {
|
|
|
90
159
|
}
|
|
91
160
|
throw new Error(`Unsupported arrow function body: ${node.getText().slice(0, 80)}`);
|
|
92
161
|
}
|
|
93
|
-
// Array literal: [a, b, c] → arrayLiteral,
|
|
162
|
+
// Array literal: [a, b, c] → arrayLiteral, with spreads → concatenation
|
|
94
163
|
if (Node.isArrayLiteralExpression(node)) {
|
|
95
164
|
const elems = node.getElements();
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
return { kind: "
|
|
165
|
+
const hasSpread = elems.some(e => Node.isSpreadElement(e));
|
|
166
|
+
if (!hasSpread) {
|
|
167
|
+
return { kind: "arrayLiteral", elems: elems.map(e => extractExpr(e)) };
|
|
99
168
|
}
|
|
100
|
-
// [a, b, c]
|
|
101
|
-
|
|
169
|
+
// Build concatenation: [a, ...b, c] → [a] + b + [c]
|
|
170
|
+
// Group consecutive non-spread elements into array literals, spreads are bare
|
|
171
|
+
const segments = [];
|
|
172
|
+
let currentLiterals = [];
|
|
173
|
+
for (const e of elems) {
|
|
174
|
+
if (Node.isSpreadElement(e)) {
|
|
175
|
+
if (currentLiterals.length > 0) {
|
|
176
|
+
segments.push({ kind: "arrayLiteral", elems: currentLiterals });
|
|
177
|
+
currentLiterals = [];
|
|
178
|
+
}
|
|
179
|
+
segments.push(extractExpr(e.getExpression()));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
currentLiterals.push(extractExpr(e));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (currentLiterals.length > 0) {
|
|
186
|
+
segments.push({ kind: "arrayLiteral", elems: currentLiterals });
|
|
187
|
+
}
|
|
188
|
+
// Fold segments with arrayConcat
|
|
189
|
+
let result = segments[0];
|
|
190
|
+
for (let i = 1; i < segments.length; i++) {
|
|
191
|
+
result = { kind: "binop", op: "arrayConcat", left: result, right: segments[i] };
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
102
194
|
}
|
|
103
195
|
// Object literal: { res: true, done: false } or { ...obj, res: true }
|
|
104
196
|
if (Node.isObjectLiteralExpression(node)) {
|
|
@@ -108,6 +200,10 @@ function extractExpr(node) {
|
|
|
108
200
|
if (Node.isSpreadAssignment(prop)) {
|
|
109
201
|
spread = extractExpr(prop.getExpression());
|
|
110
202
|
}
|
|
203
|
+
else if (Node.isShorthandPropertyAssignment(prop)) {
|
|
204
|
+
const name = prop.getName();
|
|
205
|
+
fields.push({ name, value: { kind: "var", name } });
|
|
206
|
+
}
|
|
111
207
|
else if (Node.isPropertyAssignment(prop)) {
|
|
112
208
|
const init = prop.getInitializer();
|
|
113
209
|
if (init)
|
|
@@ -120,6 +216,51 @@ function extractExpr(node) {
|
|
|
120
216
|
if (Node.isConditionalExpression(node)) {
|
|
121
217
|
return { kind: "conditional", cond: extractExpr(node.getCondition()), then: extractExpr(node.getWhenTrue()), else: extractExpr(node.getWhenFalse()) };
|
|
122
218
|
}
|
|
219
|
+
// Non-null assertion: expr!
|
|
220
|
+
if (Node.isNonNullExpression(node)) {
|
|
221
|
+
return { kind: "nonNull", expr: extractExpr(node.getExpression()) };
|
|
222
|
+
}
|
|
223
|
+
// new Map<K,V>() / new Set<T>() — with or without initializer
|
|
224
|
+
if (Node.isNewExpression(node)) {
|
|
225
|
+
const name = node.getExpression().getText();
|
|
226
|
+
if (name === "Map" || name === "Set") {
|
|
227
|
+
const typeArgs = node.getTypeArguments();
|
|
228
|
+
// Use explicit type args if present, otherwise infer from TS type system
|
|
229
|
+
const tsType = typeArgs && typeArgs.length > 0
|
|
230
|
+
? `${name}<${typeArgs.map(t => t.getText()).join(", ")}>`
|
|
231
|
+
: _eraseGenerics(typeToString(node.getType()));
|
|
232
|
+
const args = node.getArguments();
|
|
233
|
+
// new Map(source) — clone existing map or build from entries
|
|
234
|
+
if (name === "Map" && args && args.length === 1) {
|
|
235
|
+
const argType = args[0].getType();
|
|
236
|
+
const argSymbol = argType.getSymbol()?.getName() ?? argType.getAliasSymbol()?.getName();
|
|
237
|
+
if (argSymbol === "Map") {
|
|
238
|
+
// new Map(existingMap) — identity (Dafny maps are value types)
|
|
239
|
+
return extractExpr(args[0]);
|
|
240
|
+
}
|
|
241
|
+
// new Map(entries) — map-from-array constructor
|
|
242
|
+
return { kind: "call", fn: { kind: "var", name: "__mapFromArray" }, args: [extractExpr(args[0])] };
|
|
243
|
+
}
|
|
244
|
+
// new Set([a, b, c]) — set with initial elements
|
|
245
|
+
if (name === "Set" && args && args.length === 1) {
|
|
246
|
+
const arg = args[0];
|
|
247
|
+
if (Node.isArrayLiteralExpression(arg)) {
|
|
248
|
+
return { kind: "emptyCollection", collectionType: "Set", tsType, initElems: arg.getElements().map(e => extractExpr(e)) };
|
|
249
|
+
}
|
|
250
|
+
// new Set(existingSet) — pass through
|
|
251
|
+
return extractExpr(arg);
|
|
252
|
+
}
|
|
253
|
+
return { kind: "emptyCollection", collectionType: name, tsType };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// As-expression: expr as T — strip the type assertion
|
|
257
|
+
if (Node.isAsExpression(node)) {
|
|
258
|
+
return extractExpr(node.getExpression());
|
|
259
|
+
}
|
|
260
|
+
// null → undefined (both map to None in backends)
|
|
261
|
+
if (Node.isNullLiteral(node)) {
|
|
262
|
+
return { kind: "var", name: "undefined" };
|
|
263
|
+
}
|
|
123
264
|
throw new Error(`Unsupported expression: ${node.getText()}`);
|
|
124
265
|
}
|
|
125
266
|
// ── Annotation parsing ───────────────────────────────────────
|
|
@@ -149,13 +290,15 @@ function collectAnnotations(node, body) {
|
|
|
149
290
|
return own;
|
|
150
291
|
}
|
|
151
292
|
// ── Type declaration extraction ──────────────────────────────
|
|
152
|
-
function extractTypeDecl(decl) {
|
|
293
|
+
function extractTypeDecl(decl, extraDecls) {
|
|
153
294
|
const name = decl.getName();
|
|
154
295
|
const type = decl.getType();
|
|
296
|
+
const typeParams = decl.getTypeParameters().map(tp => tp.getName());
|
|
297
|
+
const tpField = typeParams.length > 0 ? typeParams : undefined;
|
|
155
298
|
if (type.isUnion()) {
|
|
156
299
|
const members = type.getUnionTypes();
|
|
157
300
|
if (members.every(m => m.isStringLiteral())) {
|
|
158
|
-
return { name, kind: "string-union", values: members.map(m => m.getLiteralValue()) };
|
|
301
|
+
return { name, typeParams: tpField, kind: "string-union", values: members.map(m => m.getLiteralValue()) };
|
|
159
302
|
}
|
|
160
303
|
if (members.every(m => m.isObject())) {
|
|
161
304
|
const discriminant = findDiscriminant(members);
|
|
@@ -163,7 +306,8 @@ function extractTypeDecl(decl) {
|
|
|
163
306
|
const variants = members.map(m => {
|
|
164
307
|
const tagProp = m.getProperty(discriminant);
|
|
165
308
|
const tagType = tagProp?.getTypeAtLocation(decl);
|
|
166
|
-
const tag = tagType?.getLiteralValue()
|
|
309
|
+
const tag = tagType?.isStringLiteral() ? String(tagType.getLiteralValue())
|
|
310
|
+
: tagType?.getText() ?? "unknown";
|
|
167
311
|
const fields = [];
|
|
168
312
|
for (const prop of m.getProperties()) {
|
|
169
313
|
if (prop.getName() === discriminant)
|
|
@@ -172,15 +316,19 @@ function extractTypeDecl(decl) {
|
|
|
172
316
|
}
|
|
173
317
|
return { name: tag, fields };
|
|
174
318
|
});
|
|
175
|
-
return { name, kind: "discriminated-union", discriminant, variants };
|
|
319
|
+
return { name, typeParams: tpField, kind: "discriminated-union", discriminant, variants };
|
|
176
320
|
}
|
|
177
321
|
}
|
|
178
322
|
}
|
|
179
|
-
if (type.isObject())
|
|
180
|
-
return extractRecord(name, type, decl);
|
|
323
|
+
if (type.isObject() || type.isIntersection())
|
|
324
|
+
return extractRecord(name, type, decl, undefined, extraDecls);
|
|
325
|
+
// Primitive type alias: type TaskId = number → alias
|
|
326
|
+
const tsType = typeToString(type);
|
|
327
|
+
if (tsType !== name)
|
|
328
|
+
return { name, kind: "alias", aliasOf: tsType };
|
|
181
329
|
return null;
|
|
182
330
|
}
|
|
183
|
-
function extractInterface(decl) {
|
|
331
|
+
function extractInterface(decl, extraDecls) {
|
|
184
332
|
// Collect field type overrides from trailing //@ type annotations
|
|
185
333
|
const overrides = new Map();
|
|
186
334
|
for (const member of decl.getMembers()) {
|
|
@@ -191,16 +339,44 @@ function extractInterface(decl) {
|
|
|
191
339
|
overrides.set(member.getName(), match[1]);
|
|
192
340
|
}
|
|
193
341
|
}
|
|
194
|
-
return extractRecord(decl.getName(), decl.getType(), decl, overrides);
|
|
342
|
+
return extractRecord(decl.getName(), decl.getType(), decl, overrides, extraDecls);
|
|
195
343
|
}
|
|
196
|
-
function extractRecord(name, type, locationNode, overrides) {
|
|
344
|
+
function extractRecord(name, type, locationNode, overrides, extraDecls) {
|
|
197
345
|
const props = type.getProperties();
|
|
198
346
|
if (props.length === 0)
|
|
199
347
|
return null;
|
|
200
348
|
const fields = [];
|
|
201
349
|
for (const prop of props) {
|
|
202
350
|
const override = overrides?.get(prop.getName());
|
|
203
|
-
|
|
351
|
+
if (override) {
|
|
352
|
+
fields.push({ name: prop.getName(), tsType: override });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const propType = prop.getTypeAtLocation(locationNode);
|
|
356
|
+
let tsType = typeToString(propType);
|
|
357
|
+
// Inline anonymous object types: ts-morph names them __type.
|
|
358
|
+
// Generate a synthetic named record and reference it by name instead.
|
|
359
|
+
if (extraDecls && tsType.includes("__type")) {
|
|
360
|
+
let innerType = propType;
|
|
361
|
+
let isOptional = false;
|
|
362
|
+
if (propType.isUnion()) {
|
|
363
|
+
const uTypes = propType.getUnionTypes();
|
|
364
|
+
const nonUndef = uTypes.filter(t => !t.isUndefined());
|
|
365
|
+
if (uTypes.some(t => t.isUndefined()) && nonUndef.length === 1) {
|
|
366
|
+
innerType = nonUndef[0];
|
|
367
|
+
isOptional = true;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (innerType.isObject() && !innerType.isArray()) {
|
|
371
|
+
const fName = prop.getName();
|
|
372
|
+
const synName = name + fName.charAt(0).toUpperCase() + fName.slice(1);
|
|
373
|
+
const extracted = extractRecord(synName, innerType, locationNode, undefined, extraDecls);
|
|
374
|
+
if (extracted) {
|
|
375
|
+
extraDecls.push(extracted);
|
|
376
|
+
tsType = isOptional ? `${synName} | undefined` : synName;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
204
380
|
fields.push({ name: prop.getName(), tsType });
|
|
205
381
|
}
|
|
206
382
|
return { name, kind: "record", fields };
|
|
@@ -216,7 +392,7 @@ function findDiscriminant(members) {
|
|
|
216
392
|
if (!p)
|
|
217
393
|
return false;
|
|
218
394
|
const t = p.getDeclarations()[0] ? p.getTypeAtLocation(p.getDeclarations()[0]) : null;
|
|
219
|
-
return t?.isStringLiteral() ?? false;
|
|
395
|
+
return (t?.isStringLiteral() || t?.isBooleanLiteral()) ?? false;
|
|
220
396
|
});
|
|
221
397
|
if (allHave)
|
|
222
398
|
return name;
|
|
@@ -224,39 +400,136 @@ function findDiscriminant(members) {
|
|
|
224
400
|
return null;
|
|
225
401
|
}
|
|
226
402
|
function typeToString(type) {
|
|
227
|
-
if (type.
|
|
403
|
+
if (type.isUndefined())
|
|
404
|
+
return "undefined";
|
|
405
|
+
if (type.isNumber() || type.isNumberLiteral())
|
|
228
406
|
return "number";
|
|
229
|
-
if (type.
|
|
407
|
+
if (type.isBigInt() || type.isBigIntLiteral())
|
|
408
|
+
return "bigint";
|
|
409
|
+
if (type.isString() || type.isStringLiteral())
|
|
230
410
|
return "string";
|
|
231
411
|
if (type.isBoolean())
|
|
232
412
|
return "boolean";
|
|
413
|
+
// Named type alias (e.g. Priority = "low" | "medium" | "high") — use the alias name
|
|
414
|
+
if (type.getAliasSymbol()) {
|
|
415
|
+
const name = type.getAliasSymbol().getName();
|
|
416
|
+
const args = type.getAliasTypeArguments();
|
|
417
|
+
if (args.length > 0)
|
|
418
|
+
return `${name}<${args.map(t => typeToString(t)).join(", ")}>`;
|
|
419
|
+
return name;
|
|
420
|
+
}
|
|
421
|
+
if (type.isUnion()) {
|
|
422
|
+
const parts = [...new Set(type.getUnionTypes().map(typeToString))];
|
|
423
|
+
return parts.join(" | ");
|
|
424
|
+
}
|
|
425
|
+
if (type.isTuple()) {
|
|
426
|
+
return `[${type.getTupleElements().map(t => typeToString(t)).join(", ")}]`;
|
|
427
|
+
}
|
|
233
428
|
if (type.isArray()) {
|
|
234
429
|
const elem = type.getArrayElementTypeOrThrow();
|
|
235
430
|
return `${typeToString(elem)}[]`;
|
|
236
431
|
}
|
|
237
432
|
const symbol = type.getSymbol() ?? type.getAliasSymbol();
|
|
238
|
-
if (symbol)
|
|
239
|
-
|
|
433
|
+
if (symbol) {
|
|
434
|
+
const name = symbol.getName();
|
|
435
|
+
const typeArgs = type.getTypeArguments();
|
|
436
|
+
if (typeArgs.length > 0) {
|
|
437
|
+
return `${name}<${typeArgs.map(t => typeToString(t)).join(", ")}>`;
|
|
438
|
+
}
|
|
439
|
+
return name;
|
|
440
|
+
}
|
|
240
441
|
return type.getText();
|
|
241
442
|
}
|
|
242
443
|
const COMPOUND_OPS = {
|
|
243
444
|
"+=": "+", "-=": "-", "*=": "*", "/=": "/", "%=": "%",
|
|
244
445
|
};
|
|
245
446
|
// ── Statement extraction ─────────────────────────────────────
|
|
447
|
+
/** Parse ghost and assert annotations from comment ranges. */
|
|
448
|
+
function parseSpecComments(ranges, line) {
|
|
449
|
+
const result = [];
|
|
450
|
+
for (const range of ranges) {
|
|
451
|
+
const text = range.getText().trim();
|
|
452
|
+
if (!text.startsWith(PREFIX))
|
|
453
|
+
continue;
|
|
454
|
+
const content = text.slice(PREFIX.length);
|
|
455
|
+
// assert expr
|
|
456
|
+
if (content.startsWith("assert ")) {
|
|
457
|
+
result.push({ kind: "assert", expr: content.slice(7).trim(), line });
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (!content.startsWith("ghost "))
|
|
461
|
+
continue;
|
|
462
|
+
const ghostBody = content.slice(6).trim();
|
|
463
|
+
// ghost let varName: type = expr OR ghost let varName = expr
|
|
464
|
+
const letMatch = ghostBody.match(/^let\s+(\w+)(?:\s*:\s*(\w+))?\s*=\s*(.+)$/);
|
|
465
|
+
if (letMatch) {
|
|
466
|
+
result.push({ kind: "ghostLet", name: letMatch[1], tsType: letMatch[2] ?? null, init: letMatch[3].trim(), line });
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
// ghost varName = expr
|
|
470
|
+
const assignMatch = ghostBody.match(/^(\w+)\s*=\s*(.+)$/);
|
|
471
|
+
if (assignMatch) {
|
|
472
|
+
result.push({ kind: "ghostAssign", target: assignMatch[1], value: assignMatch[2].trim(), line });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
246
477
|
function extractStmts(stmts) {
|
|
247
478
|
const result = [];
|
|
248
479
|
for (const s of stmts) {
|
|
249
480
|
const line = s.getStartLineNumber();
|
|
481
|
+
// Ghost annotations from leading comments → inject before this statement
|
|
482
|
+
const leadingComments = s.getLeadingCommentRanges();
|
|
483
|
+
result.push(...parseSpecComments(leadingComments, line));
|
|
484
|
+
// //@ skip — omit this statement from the verification model
|
|
485
|
+
if (leadingComments.some(r => r.getText().trim() === "//@ skip")) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
250
488
|
if (Node.isVariableStatement(s)) {
|
|
489
|
+
const havocMatch = s.getLeadingCommentRanges()
|
|
490
|
+
.map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s*:\s*(.+)|(?:\s+(\S+)))?$/))
|
|
491
|
+
.find(m => m !== null);
|
|
492
|
+
const havocType = havocMatch?.[1]?.trim() ?? null; // //@ havoc : Type
|
|
493
|
+
const havocKey = havocMatch?.[2] ?? null; // //@ havoc key
|
|
494
|
+
const isHavoc = !!havocMatch;
|
|
251
495
|
for (const d of s.getDeclarations()) {
|
|
496
|
+
// Havoc on destructuring: emit each named binding as a separate havoced variable
|
|
497
|
+
const nameNode = d.getNameNode();
|
|
498
|
+
if (isHavoc && !havocKey && Node.isObjectBindingPattern(nameNode)) {
|
|
499
|
+
const rhsType = d.getType();
|
|
500
|
+
const havocTypes = havocType?.split(",").map(t => t.trim()) ?? [];
|
|
501
|
+
const elements = nameNode.getElements();
|
|
502
|
+
for (let ei = 0; ei < elements.length; ei++) {
|
|
503
|
+
const el = elements[ei];
|
|
504
|
+
const name = el.getName();
|
|
505
|
+
const propType = rhsType.getProperty(name)?.getTypeAtLocation(d);
|
|
506
|
+
const tsType = havocTypes[ei] ?? (propType ? _eraseGenerics(typeToString(propType)) : "unknown");
|
|
507
|
+
result.push({
|
|
508
|
+
kind: "let", name, mutable: s.getDeclarationKind() === "let",
|
|
509
|
+
tsType, init: { kind: "havoc", tsType }, line,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
252
514
|
const declType = d.getType();
|
|
253
|
-
|
|
515
|
+
let init;
|
|
516
|
+
if (isHavoc && !havocKey) {
|
|
517
|
+
// Use explicit type from //@ havoc : Type, or initializer type (pre-cast), or declared type
|
|
518
|
+
const initType = d.getInitializer()?.getType();
|
|
519
|
+
init = { kind: "havoc", tsType: havocType ?? typeToString(initType ?? declType) };
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
const initializer = d.getInitializer();
|
|
523
|
+
_havocKey = havocKey;
|
|
524
|
+
init = initializer ? extractExpr(initializer) : { kind: "var", name: "default" };
|
|
525
|
+
_havocKey = null;
|
|
526
|
+
}
|
|
254
527
|
result.push({
|
|
255
528
|
kind: "let",
|
|
256
529
|
name: d.getName(),
|
|
257
530
|
mutable: s.getDeclarationKind() === "let",
|
|
258
|
-
tsType: typeToString(declType),
|
|
259
|
-
init
|
|
531
|
+
tsType: havocType ?? _eraseGenerics(typeToString(declType)),
|
|
532
|
+
init,
|
|
260
533
|
line,
|
|
261
534
|
});
|
|
262
535
|
}
|
|
@@ -279,13 +552,29 @@ function extractStmts(stmts) {
|
|
|
279
552
|
}
|
|
280
553
|
if (Node.isForOfStatement(s)) {
|
|
281
554
|
const init = s.getInitializer();
|
|
282
|
-
const
|
|
555
|
+
const names = [];
|
|
556
|
+
if (Node.isVariableDeclarationList(init)) {
|
|
557
|
+
const decl = init.getDeclarations()[0];
|
|
558
|
+
const nameNode = decl?.getNameNode();
|
|
559
|
+
if (nameNode && Node.isArrayBindingPattern(nameNode)) {
|
|
560
|
+
for (const elem of nameNode.getElements()) {
|
|
561
|
+
if (Node.isBindingElement(elem))
|
|
562
|
+
names.push(elem.getNameNode().getText());
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
names.push(decl?.getName() ?? "_");
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
names.push("_");
|
|
571
|
+
}
|
|
283
572
|
const bodyNode = s.getStatement();
|
|
284
573
|
const bodyStmts = Node.isBlock(bodyNode) ? bodyNode.getStatements() : [bodyNode];
|
|
285
574
|
const annots = collectAnnotations(s, bodyStmts);
|
|
286
575
|
result.push({
|
|
287
576
|
kind: "forof",
|
|
288
|
-
|
|
577
|
+
names,
|
|
289
578
|
iterable: extractExpr(s.getExpression()),
|
|
290
579
|
invariants: annots.filter(a => a.kind === "invariant").map(a => a.expr),
|
|
291
580
|
doneWith: annots.find(a => a.kind === "done_with")?.expr ?? null,
|
|
@@ -344,8 +633,18 @@ function extractStmts(stmts) {
|
|
|
344
633
|
}
|
|
345
634
|
if (Node.isExpressionStatement(s)) {
|
|
346
635
|
const expr = s.getExpression();
|
|
347
|
-
//
|
|
348
|
-
if (Node.isBinaryExpression(expr) && expr.getOperatorToken().getText() === "=") {
|
|
636
|
+
// arr[i] = v → arr = arr.with(i, v)
|
|
637
|
+
if (Node.isBinaryExpression(expr) && expr.getOperatorToken().getText() === "=" && Node.isElementAccessExpression(expr.getLeft())) {
|
|
638
|
+
const left = expr.getLeft();
|
|
639
|
+
const obj = extractExpr(left.getExpression());
|
|
640
|
+
const idx = extractExpr(left.getArgumentExpression());
|
|
641
|
+
const val = extractExpr(expr.getRight());
|
|
642
|
+
const target = left.getExpression().getText();
|
|
643
|
+
const withCall = { kind: "call", fn: { kind: "field", obj, field: "with" }, args: [idx, val] };
|
|
644
|
+
result.push({ kind: "assign", target, value: withCall, line });
|
|
645
|
+
// x = e
|
|
646
|
+
}
|
|
647
|
+
else if (Node.isBinaryExpression(expr) && expr.getOperatorToken().getText() === "=") {
|
|
349
648
|
result.push({ kind: "assign", target: expr.getLeft().getText(), value: extractExpr(expr.getRight()), line });
|
|
350
649
|
// x += e, x -= e, etc.
|
|
351
650
|
}
|
|
@@ -371,17 +670,77 @@ function extractStmts(stmts) {
|
|
|
371
670
|
}
|
|
372
671
|
continue;
|
|
373
672
|
}
|
|
673
|
+
if (Node.isThrowStatement(s)) {
|
|
674
|
+
result.push({ kind: "throw", line });
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
// Block statement: { ... } — flatten into parent
|
|
678
|
+
if (Node.isBlock(s)) {
|
|
679
|
+
result.push(...extractStmts(s.getStatements()));
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
374
682
|
throw new Error(`Unsupported statement at line ${line}: ${s.getText().slice(0, 80)}`);
|
|
375
683
|
}
|
|
684
|
+
// Ghost comments after the last statement (before closing brace) appear as sibling trivia nodes
|
|
685
|
+
if (stmts.length > 0) {
|
|
686
|
+
const last = stmts[stmts.length - 1];
|
|
687
|
+
const line = last.getStartLineNumber();
|
|
688
|
+
for (const sib of last.getNextSiblings()) {
|
|
689
|
+
const text = sib.getText().trim();
|
|
690
|
+
if (!text.startsWith(PREFIX))
|
|
691
|
+
continue;
|
|
692
|
+
const content = text.slice(PREFIX.length);
|
|
693
|
+
// assert expr
|
|
694
|
+
if (content.startsWith("assert ")) {
|
|
695
|
+
result.push({ kind: "assert", expr: content.slice(7).trim(), line });
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (!content.startsWith("ghost "))
|
|
699
|
+
continue;
|
|
700
|
+
const ghostBody = content.slice(6).trim();
|
|
701
|
+
const letMatch = ghostBody.match(/^let\s+(\w+)(?:\s*:\s*(\w+))?\s*=\s*(.+)$/);
|
|
702
|
+
if (letMatch) {
|
|
703
|
+
result.push({ kind: "ghostLet", name: letMatch[1], tsType: letMatch[2] ?? null, init: letMatch[3].trim(), line });
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const assignMatch = ghostBody.match(/^(\w+)\s*=\s*(.+)$/);
|
|
707
|
+
if (assignMatch) {
|
|
708
|
+
result.push({ kind: "ghostAssign", target: assignMatch[1], value: assignMatch[2].trim(), line });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
376
712
|
return result;
|
|
377
713
|
}
|
|
378
714
|
// ── Function extraction ──────────────────────────────────────
|
|
379
|
-
function extractFunction(fn) {
|
|
715
|
+
function extractFunction(fn, parentAnnotations) {
|
|
716
|
+
// Generic bounds erasure: <T extends Base> → substitute T with Base everywhere
|
|
717
|
+
// Unbounded type params are preserved as Dafny type parameters
|
|
718
|
+
_typeParamMap = new Map();
|
|
719
|
+
const unboundedTypeParams = [];
|
|
720
|
+
for (const tp of fn.getTypeParameters?.() ?? []) {
|
|
721
|
+
const constraint = tp.getConstraint();
|
|
722
|
+
if (constraint)
|
|
723
|
+
_typeParamMap.set(tp.getName(), constraint.getText());
|
|
724
|
+
else
|
|
725
|
+
unboundedTypeParams.push(tp.getName());
|
|
726
|
+
}
|
|
380
727
|
const body = fn.getBody();
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
728
|
+
// Expression-body arrow: wrap in implicit return
|
|
729
|
+
let extractedBody;
|
|
730
|
+
let annots;
|
|
731
|
+
if (body && !Node.isBlock(body)) {
|
|
732
|
+
const expr = extractExpr(body);
|
|
733
|
+
extractedBody = [{ kind: "return", value: expr, line: body.getStartLineNumber() }];
|
|
734
|
+
annots = parentAnnotations ?? collectAnnotations(fn);
|
|
735
|
+
}
|
|
736
|
+
else if (body && Node.isBlock(body)) {
|
|
737
|
+
const bodyStmts = body.getStatements();
|
|
738
|
+
extractedBody = extractStmts(bodyStmts);
|
|
739
|
+
annots = collectAnnotations(fn, bodyStmts);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
throw new Error(`${fn.getName?.() ?? "arrow"}: function has no body`);
|
|
743
|
+
}
|
|
385
744
|
const typeAnnotations = [];
|
|
386
745
|
for (const a of annots) {
|
|
387
746
|
if (a.kind === "type") {
|
|
@@ -391,36 +750,478 @@ function extractFunction(fn) {
|
|
|
391
750
|
}
|
|
392
751
|
}
|
|
393
752
|
return {
|
|
394
|
-
name: fn.getName() ?? "<anonymous>",
|
|
395
|
-
|
|
396
|
-
|
|
753
|
+
name: fn.getName?.() ?? "<anonymous>",
|
|
754
|
+
typeParams: unboundedTypeParams,
|
|
755
|
+
params: fn.getParameters().flatMap(p => {
|
|
756
|
+
// Flatten destructured object params into individual params
|
|
757
|
+
const nameNode = p.getNameNode();
|
|
758
|
+
if (Node.isObjectBindingPattern(nameNode)) {
|
|
759
|
+
const type = p.getType();
|
|
760
|
+
return nameNode.getElements().map(el => {
|
|
761
|
+
const name = el.getName();
|
|
762
|
+
const propType = type.getProperty(name)?.getTypeAtLocation(p);
|
|
763
|
+
return { name, tsType: propType ? typeToString(propType) : "unknown" };
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
let tsType = _eraseGenerics(p.getTypeNode()?.getText() ?? "unknown");
|
|
767
|
+
// Optional parameters (foo?: T) need | undefined in the type string
|
|
768
|
+
if (p.hasQuestionToken())
|
|
769
|
+
tsType = `${tsType} | undefined`;
|
|
770
|
+
return [{ name: p.getName(), tsType }];
|
|
771
|
+
}),
|
|
772
|
+
returnType: (() => {
|
|
773
|
+
const node = fn.getReturnTypeNode();
|
|
774
|
+
if (node)
|
|
775
|
+
return _eraseGenerics(node.getText());
|
|
776
|
+
const inferred = fn.getReturnType();
|
|
777
|
+
if (inferred.isAny())
|
|
778
|
+
return "unknown";
|
|
779
|
+
return _eraseGenerics(typeToString(inferred));
|
|
780
|
+
})(),
|
|
397
781
|
requires: annots.filter(a => a.kind === "requires").map(a => a.expr),
|
|
398
782
|
ensures: annots.filter(a => a.kind === "ensures").map(a => a.expr),
|
|
399
783
|
typeAnnotations,
|
|
400
|
-
body:
|
|
784
|
+
body: extractedBody,
|
|
401
785
|
line: fn.getStartLineNumber(),
|
|
402
786
|
};
|
|
403
787
|
}
|
|
404
788
|
// ── Module extraction ────────────────────────────────────────
|
|
405
789
|
export function extractModule(sourceFile) {
|
|
406
790
|
const typeDecls = [];
|
|
791
|
+
// Parse //@ declare-type directives from file comments
|
|
792
|
+
for (const range of sourceFile.getLeadingCommentRanges()) {
|
|
793
|
+
const text = range.getText().trim();
|
|
794
|
+
if (!text.startsWith("//@ declare-type "))
|
|
795
|
+
continue;
|
|
796
|
+
const body = text.slice("//@ declare-type ".length);
|
|
797
|
+
const match = body.match(/^(\w+)\s*\{(.+)\}$/);
|
|
798
|
+
if (!match)
|
|
799
|
+
continue;
|
|
800
|
+
const name = match[1];
|
|
801
|
+
const fieldsStr = match[2];
|
|
802
|
+
const fields = fieldsStr.split(",").map(f => f.trim()).filter(Boolean).map(f => {
|
|
803
|
+
const [fname, ftype] = f.split(":").map(s => s.trim());
|
|
804
|
+
return { name: fname, tsType: ftype };
|
|
805
|
+
});
|
|
806
|
+
typeDecls.push({ name, kind: "record", fields });
|
|
807
|
+
}
|
|
808
|
+
// Also scan statement-level comments for declare-type
|
|
809
|
+
for (const stmt of sourceFile.getStatements()) {
|
|
810
|
+
for (const range of stmt.getLeadingCommentRanges()) {
|
|
811
|
+
const text = range.getText().trim();
|
|
812
|
+
if (!text.startsWith("//@ declare-type "))
|
|
813
|
+
continue;
|
|
814
|
+
const body = text.slice("//@ declare-type ".length);
|
|
815
|
+
const match = body.match(/^(\w+)\s*\{(.+)\}$/);
|
|
816
|
+
if (!match)
|
|
817
|
+
continue;
|
|
818
|
+
const name = match[1];
|
|
819
|
+
const fields = match[2].split(",").map(f => f.trim()).filter(Boolean).map(f => {
|
|
820
|
+
const [fname, ftype] = f.split(":").map(s => s.trim());
|
|
821
|
+
return { name: fname, tsType: ftype };
|
|
822
|
+
});
|
|
823
|
+
typeDecls.push({ name, kind: "record", fields });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
407
826
|
// Extract type declarations in source order to respect dependencies
|
|
827
|
+
// Skip types already declared via //@ declare-type
|
|
828
|
+
const declaredNames = new Set(typeDecls.map(d => d.name));
|
|
408
829
|
for (const stmt of sourceFile.getStatements()) {
|
|
409
|
-
if (Node.isTypeAliasDeclaration(stmt)) {
|
|
410
|
-
const
|
|
830
|
+
if (Node.isTypeAliasDeclaration(stmt) && !declaredNames.has(stmt.getName())) {
|
|
831
|
+
const extra = [];
|
|
832
|
+
const info = extractTypeDecl(stmt, extra);
|
|
833
|
+
typeDecls.push(...extra);
|
|
411
834
|
if (info)
|
|
412
835
|
typeDecls.push(info);
|
|
413
836
|
}
|
|
414
|
-
else if (Node.isInterfaceDeclaration(stmt)) {
|
|
415
|
-
const
|
|
837
|
+
else if (Node.isInterfaceDeclaration(stmt) && !declaredNames.has(stmt.getName())) {
|
|
838
|
+
const extra = [];
|
|
839
|
+
const info = extractInterface(stmt, extra);
|
|
840
|
+
// Synthetic types from inline objects must precede the parent type
|
|
841
|
+
typeDecls.push(...extra);
|
|
416
842
|
if (info)
|
|
417
843
|
typeDecls.push(info);
|
|
418
844
|
}
|
|
419
845
|
}
|
|
846
|
+
// Extract module-level const declarations
|
|
847
|
+
const constants = [];
|
|
848
|
+
for (const stmt of sourceFile.getStatements()) {
|
|
849
|
+
if (Node.isVariableStatement(stmt)) {
|
|
850
|
+
for (const decl of stmt.getDeclarationList().getDeclarations()) {
|
|
851
|
+
if (stmt.getDeclarationList().getFlags() & 2 /* const */) {
|
|
852
|
+
const init = decl.getInitializer();
|
|
853
|
+
// Skip huge string constants — they crash the verifier and have no verification value
|
|
854
|
+
const initType = decl.getType();
|
|
855
|
+
const isHugeString = (initType.isString() || initType.isStringLiteral()) && init.getText().length > 200;
|
|
856
|
+
if (init && !isHugeString && !Node.isArrowFunction(init)) {
|
|
857
|
+
try {
|
|
858
|
+
constants.push({
|
|
859
|
+
name: decl.getName(),
|
|
860
|
+
tsType: typeToString(decl.getType()),
|
|
861
|
+
value: extractExpr(init),
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
catch (e) {
|
|
865
|
+
console.error(`WARNING: skipping const '${decl.getName()}': ${e.message}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
// Collect all function-like declarations: function declarations + const arrow functions
|
|
873
|
+
const allFns = [];
|
|
874
|
+
for (const fn of sourceFile.getFunctions()) {
|
|
875
|
+
allFns.push({ name: fn.getName() ?? "<anonymous>", node: fn });
|
|
876
|
+
}
|
|
877
|
+
// const f = (...) => expr OR const f = (...) => { ... }
|
|
878
|
+
for (const stmt of sourceFile.getStatements()) {
|
|
879
|
+
if (Node.isVariableStatement(stmt)) {
|
|
880
|
+
for (const decl of stmt.getDeclarationList().getDeclarations()) {
|
|
881
|
+
const init = decl.getInitializer();
|
|
882
|
+
if (init && Node.isArrowFunction(init)) {
|
|
883
|
+
allFns.push({ name: decl.getName(), node: init, parentStmt: stmt });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// If any function has //@ verify, only extract those (brownfield mode).
|
|
889
|
+
// For expression-body arrows, //@ verify may be on the parent variable statement.
|
|
890
|
+
function hasVerify(f) {
|
|
891
|
+
if (f.node.getFullText().includes('//@ verify'))
|
|
892
|
+
return true;
|
|
893
|
+
if (f.parentStmt) {
|
|
894
|
+
for (const r of f.parentStmt.getLeadingCommentRanges()) {
|
|
895
|
+
if (r.getText().includes('//@ verify'))
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
const hasVerifyDirective = allFns.some(hasVerify);
|
|
902
|
+
const fnsToExtract = hasVerifyDirective ? allFns.filter(hasVerify) : allFns;
|
|
903
|
+
const functions = fnsToExtract.map(f => {
|
|
904
|
+
// For expression-body arrows, annotations come from the parent variable statement
|
|
905
|
+
const parentAnnots = f.parentStmt ? parseAnnotations(f.parentStmt) : undefined;
|
|
906
|
+
const raw = extractFunction(f.node, parentAnnots);
|
|
907
|
+
raw.name = f.name; // use the const name, not "<anonymous>"
|
|
908
|
+
return raw;
|
|
909
|
+
});
|
|
910
|
+
// Resolve imported type names referenced in function signatures and type fields
|
|
911
|
+
const knownTypeNames = new Set(typeDecls.map(d => d.name));
|
|
912
|
+
const primitives = new Set(["number", "string", "boolean", "void", "unknown", "undefined"]);
|
|
913
|
+
const builtinTypes = new Set(["Map", "Set", "Array", "Record", "Promise", "Date", "RegExp", "Error"]);
|
|
914
|
+
function resolveTypeName(name) {
|
|
915
|
+
if (knownTypeNames.has(name) || primitives.has(name) || builtinTypes.has(name))
|
|
916
|
+
return;
|
|
917
|
+
for (const sf2 of sourceFile.getProject().getSourceFiles()) {
|
|
918
|
+
for (const stmt of sf2.getStatements()) {
|
|
919
|
+
if (Node.isTypeAliasDeclaration(stmt) && stmt.getName() === name) {
|
|
920
|
+
const extra = [];
|
|
921
|
+
const info = extractTypeDecl(stmt, extra);
|
|
922
|
+
typeDecls.push(...extra);
|
|
923
|
+
if (info) {
|
|
924
|
+
typeDecls.push(info);
|
|
925
|
+
knownTypeNames.add(name);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (Node.isInterfaceDeclaration(stmt) && stmt.getName() === name && !knownTypeNames.has(name)) {
|
|
929
|
+
const extra = [];
|
|
930
|
+
const info = extractInterface(stmt, extra);
|
|
931
|
+
typeDecls.push(...extra);
|
|
932
|
+
if (info) {
|
|
933
|
+
typeDecls.push(info);
|
|
934
|
+
knownTypeNames.add(name);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (knownTypeNames.has(name))
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
// Recursively resolve types referenced by the newly added type's fields
|
|
942
|
+
const decl = typeDecls.find(d => d.name === name);
|
|
943
|
+
if (decl?.fields) {
|
|
944
|
+
for (const f of decl.fields) {
|
|
945
|
+
for (const m of f.tsType.matchAll(/\b([A-Z]\w*)\b/g))
|
|
946
|
+
resolveTypeName(m[1]);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (decl?.variants) {
|
|
950
|
+
for (const v of decl.variants) {
|
|
951
|
+
for (const f of v.fields) {
|
|
952
|
+
for (const m of f.tsType.matchAll(/\b([A-Z]\w*)\b/g))
|
|
953
|
+
resolveTypeName(m[1]);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
for (const fn of functions) {
|
|
959
|
+
const refs = [fn.returnType, ...fn.params.map(p => p.tsType)];
|
|
960
|
+
for (const ref of refs) {
|
|
961
|
+
for (const m of ref.matchAll(/\b([A-Z]\w*)\b/g))
|
|
962
|
+
resolveTypeName(m[1]);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
// Resolve union param types: A | B → intersection of fields
|
|
966
|
+
const typeDeclMap = new Map(typeDecls.map(d => [d.name, d]));
|
|
967
|
+
for (const fn of functions) {
|
|
968
|
+
for (const p of fn.params) {
|
|
969
|
+
if (!p.tsType.includes(" | "))
|
|
970
|
+
continue;
|
|
971
|
+
const arms = p.tsType.split(" | ").map(a => a.trim());
|
|
972
|
+
const armDecls = arms.map(a => typeDeclMap.get(a)).filter((d) => !!d && d.kind === "record");
|
|
973
|
+
if (armDecls.length < 2 || armDecls.length !== arms.length)
|
|
974
|
+
continue;
|
|
975
|
+
// Compute field name intersection
|
|
976
|
+
const fieldSets = armDecls.map(d => new Set(d.fields.map(f => f.name)));
|
|
977
|
+
const common = [...fieldSets[0]].filter(name => fieldSets.every(s => s.has(name)));
|
|
978
|
+
// Find an existing type that matches, or use the first arm's fields
|
|
979
|
+
const match = armDecls.find(d => d.fields.length === common.length && d.fields.every(f => common.includes(f.name)));
|
|
980
|
+
if (match) {
|
|
981
|
+
p.tsType = match.name;
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
// Generate synthetic union type with intersected fields
|
|
985
|
+
const synName = arms.join("Or");
|
|
986
|
+
if (!typeDeclMap.has(synName)) {
|
|
987
|
+
const fields = common.map(name => {
|
|
988
|
+
const f = armDecls[0].fields.find(f => f.name === name);
|
|
989
|
+
return { name: f.name, tsType: f.tsType };
|
|
990
|
+
});
|
|
991
|
+
typeDecls.push({ name: synName, kind: "record", fields });
|
|
992
|
+
typeDeclMap.set(synName, typeDecls[typeDecls.length - 1]);
|
|
993
|
+
}
|
|
994
|
+
p.tsType = synName;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// In brownfield mode, filter consts to only those referenced by verified functions.
|
|
999
|
+
if (hasVerifyDirective) {
|
|
1000
|
+
const referencedNames = new Set();
|
|
1001
|
+
function collectNames(stmts) {
|
|
1002
|
+
for (const s of stmts) {
|
|
1003
|
+
if (s.kind === "let") {
|
|
1004
|
+
referencedNames.add(s.tsType);
|
|
1005
|
+
collectNamesExpr(s.init);
|
|
1006
|
+
}
|
|
1007
|
+
if (s.kind === "assign") {
|
|
1008
|
+
collectNamesExpr(s.value);
|
|
1009
|
+
}
|
|
1010
|
+
if (s.kind === "return") {
|
|
1011
|
+
collectNamesExpr(s.value);
|
|
1012
|
+
}
|
|
1013
|
+
if (s.kind === "if") {
|
|
1014
|
+
collectNamesExpr(s.cond);
|
|
1015
|
+
collectNames(s.then);
|
|
1016
|
+
collectNames(s.else);
|
|
1017
|
+
}
|
|
1018
|
+
if (s.kind === "while") {
|
|
1019
|
+
collectNamesExpr(s.cond);
|
|
1020
|
+
collectNames(s.body);
|
|
1021
|
+
}
|
|
1022
|
+
if (s.kind === "forof") {
|
|
1023
|
+
collectNamesExpr(s.iterable);
|
|
1024
|
+
collectNames(s.body);
|
|
1025
|
+
}
|
|
1026
|
+
if (s.kind === "expr") {
|
|
1027
|
+
collectNamesExpr(s.expr);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function collectNamesExpr(e) {
|
|
1032
|
+
if (e.kind === "var")
|
|
1033
|
+
referencedNames.add(e.name);
|
|
1034
|
+
if (e.kind === "binop") {
|
|
1035
|
+
collectNamesExpr(e.left);
|
|
1036
|
+
collectNamesExpr(e.right);
|
|
1037
|
+
}
|
|
1038
|
+
if (e.kind === "unop") {
|
|
1039
|
+
collectNamesExpr(e.expr);
|
|
1040
|
+
}
|
|
1041
|
+
if (e.kind === "call") {
|
|
1042
|
+
collectNamesExpr(e.fn);
|
|
1043
|
+
e.args.forEach(collectNamesExpr);
|
|
1044
|
+
}
|
|
1045
|
+
if (e.kind === "field") {
|
|
1046
|
+
collectNamesExpr(e.obj);
|
|
1047
|
+
}
|
|
1048
|
+
if (e.kind === "index") {
|
|
1049
|
+
collectNamesExpr(e.obj);
|
|
1050
|
+
collectNamesExpr(e.idx);
|
|
1051
|
+
}
|
|
1052
|
+
if (e.kind === "record") {
|
|
1053
|
+
if (e.spread)
|
|
1054
|
+
collectNamesExpr(e.spread);
|
|
1055
|
+
e.fields.forEach(f => collectNamesExpr(f.value));
|
|
1056
|
+
}
|
|
1057
|
+
if (e.kind === "arrayLiteral") {
|
|
1058
|
+
e.elems.forEach(collectNamesExpr);
|
|
1059
|
+
}
|
|
1060
|
+
if (e.kind === "conditional") {
|
|
1061
|
+
collectNamesExpr(e.cond);
|
|
1062
|
+
collectNamesExpr(e.then);
|
|
1063
|
+
collectNamesExpr(e.else);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
for (const fn of functions) {
|
|
1067
|
+
for (const p of fn.params)
|
|
1068
|
+
referencedNames.add(p.tsType);
|
|
1069
|
+
referencedNames.add(fn.returnType);
|
|
1070
|
+
collectNames(fn.body);
|
|
1071
|
+
// Also scan spec annotations for identifier references
|
|
1072
|
+
for (const spec of [...fn.requires, ...fn.ensures]) {
|
|
1073
|
+
for (const m of spec.matchAll(/\b([a-zA-Z_]\w*)\b/g)) {
|
|
1074
|
+
referencedNames.add(m[1]);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
constants.splice(0, constants.length, ...constants.filter(c => referencedNames.has(c.name)));
|
|
1079
|
+
// Filter types to only those referenced by verified functions (transitive)
|
|
1080
|
+
const neededTypes = new Set();
|
|
1081
|
+
function markType(name) {
|
|
1082
|
+
if (neededTypes.has(name))
|
|
1083
|
+
return;
|
|
1084
|
+
const d = typeDecls.find(t => t.name === name);
|
|
1085
|
+
if (!d)
|
|
1086
|
+
return;
|
|
1087
|
+
neededTypes.add(name);
|
|
1088
|
+
for (const f of d.fields ?? [])
|
|
1089
|
+
for (const m of f.tsType.matchAll(/\b([A-Z]\w*)\b/g))
|
|
1090
|
+
markType(m[1]);
|
|
1091
|
+
for (const v of d.variants ?? [])
|
|
1092
|
+
for (const f of v.fields)
|
|
1093
|
+
for (const m of f.tsType.matchAll(/\b([A-Z]\w*)\b/g))
|
|
1094
|
+
markType(m[1]);
|
|
1095
|
+
}
|
|
1096
|
+
for (const name of referencedNames)
|
|
1097
|
+
markType(name);
|
|
1098
|
+
typeDecls.splice(0, typeDecls.length, ...typeDecls.filter(d => neededTypes.has(d.name) || declaredNames.has(d.name)));
|
|
1099
|
+
}
|
|
1100
|
+
// Resolve imported types: extract types referenced in function signatures but not in this file
|
|
1101
|
+
const knownTypes = new Set(typeDecls.map(d => d.name));
|
|
1102
|
+
const builtins = new Set(["Map", "Set", "Array", "String", "Number", "Boolean", "Promise", "Date", "RegExp", "Error"]);
|
|
1103
|
+
function resolveType(t, locationNode) {
|
|
1104
|
+
// Unwrap arrays and generics to find user-defined types
|
|
1105
|
+
if (t.isArray()) {
|
|
1106
|
+
resolveType(t.getArrayElementTypeOrThrow(), locationNode);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
// Resolve type aliases (e.g. string unions imported from other files)
|
|
1110
|
+
const alias = t.getAliasSymbol();
|
|
1111
|
+
if (alias) {
|
|
1112
|
+
const aliasName = alias.getName();
|
|
1113
|
+
if (!knownTypes.has(aliasName) && !builtins.has(aliasName) && !aliasName.startsWith("__")) {
|
|
1114
|
+
const decls = alias.getDeclarations();
|
|
1115
|
+
if (decls.length > 0 && Node.isTypeAliasDeclaration(decls[0])) {
|
|
1116
|
+
const extra = [];
|
|
1117
|
+
const info = extractTypeDecl(decls[0], extra);
|
|
1118
|
+
if (info) {
|
|
1119
|
+
typeDecls.push(...extra);
|
|
1120
|
+
typeDecls.push(info);
|
|
1121
|
+
knownTypes.add(aliasName);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
else if (t.getProperties().length > 0) {
|
|
1125
|
+
// Alias declaration not available (e.g. intersection type) — extract from properties
|
|
1126
|
+
const extra = [];
|
|
1127
|
+
const info = extractRecord(aliasName, t, locationNode, undefined, extra);
|
|
1128
|
+
if (info) {
|
|
1129
|
+
typeDecls.push(...extra);
|
|
1130
|
+
typeDecls.push(info);
|
|
1131
|
+
knownTypes.add(aliasName);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (t.isUnion()) {
|
|
1137
|
+
for (const u of t.getUnionTypes())
|
|
1138
|
+
resolveType(u, locationNode);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
for (const arg of t.getTypeArguments())
|
|
1142
|
+
resolveType(arg, locationNode);
|
|
1143
|
+
const sym = t.getSymbol() ?? t.getAliasSymbol();
|
|
1144
|
+
const name = sym?.getName();
|
|
1145
|
+
if (name && !name.startsWith("__") && !knownTypes.has(name) && !builtins.has(name) && (t.isObject() || t.isIntersection())) {
|
|
1146
|
+
const extra = [];
|
|
1147
|
+
const info = extractRecord(name, t, locationNode, undefined, extra);
|
|
1148
|
+
if (info) {
|
|
1149
|
+
typeDecls.push(...extra);
|
|
1150
|
+
typeDecls.push(info);
|
|
1151
|
+
knownTypes.add(name);
|
|
1152
|
+
// Recursively resolve types referenced in this type's fields
|
|
1153
|
+
for (const prop of t.getProperties()) {
|
|
1154
|
+
resolveType(prop.getTypeAtLocation(locationNode), locationNode);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
for (const f of fnsToExtract) {
|
|
1160
|
+
for (const p of f.node.getParameters())
|
|
1161
|
+
resolveType(p.getType(), p);
|
|
1162
|
+
}
|
|
1163
|
+
// Resolve anonymous object return types into synthetic named types
|
|
1164
|
+
for (let i = 0; i < fnsToExtract.length; i++) {
|
|
1165
|
+
const f = fnsToExtract[i];
|
|
1166
|
+
const fn = functions[i];
|
|
1167
|
+
const retType = f.node.getReturnType();
|
|
1168
|
+
// Prefer alias symbol (named type aliases) over underlying object symbol (__type)
|
|
1169
|
+
const aliasSym = retType.getAliasSymbol();
|
|
1170
|
+
if (aliasSym && !aliasSym.getName().startsWith("__")) {
|
|
1171
|
+
// Named type alias — resolve it instead of generating a synthetic name
|
|
1172
|
+
resolveType(retType, f.node);
|
|
1173
|
+
const aliasName = aliasSym.getName();
|
|
1174
|
+
if (knownTypes.has(aliasName)) {
|
|
1175
|
+
// Preserve type arguments: Result<Model, Err> not just Result
|
|
1176
|
+
const typeArgs = retType.getAliasTypeArguments();
|
|
1177
|
+
fn.returnType = typeArgs.length > 0
|
|
1178
|
+
? `${aliasName}<${typeArgs.map(t => typeToString(t)).join(", ")}>`
|
|
1179
|
+
: aliasName;
|
|
1180
|
+
}
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
const sym = retType.getSymbol();
|
|
1184
|
+
if (sym?.getName() === "__type" && retType.isObject() && !retType.isArray()) {
|
|
1185
|
+
const synName = fn.name.charAt(0).toUpperCase() + fn.name.slice(1) + "Result";
|
|
1186
|
+
if (!knownTypes.has(synName)) {
|
|
1187
|
+
const extra = [];
|
|
1188
|
+
const info = extractRecord(synName, retType, f.node, undefined, extra);
|
|
1189
|
+
if (info) {
|
|
1190
|
+
typeDecls.push(...extra);
|
|
1191
|
+
typeDecls.push(info);
|
|
1192
|
+
knownTypes.add(synName);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
fn.returnType = synName;
|
|
1196
|
+
// Also resolve imported types referenced in the return type's fields
|
|
1197
|
+
for (const prop of retType.getProperties()) {
|
|
1198
|
+
resolveType(prop.getTypeAtLocation(f.node), f.node);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
// Extract classes with //@ verify methods
|
|
1203
|
+
const classes = [];
|
|
1204
|
+
for (const cls of sourceFile.getClasses()) {
|
|
1205
|
+
const methods = [];
|
|
1206
|
+
for (const method of cls.getMethods()) {
|
|
1207
|
+
if (!method.getFullText().includes('//@ verify'))
|
|
1208
|
+
continue;
|
|
1209
|
+
methods.push(extractFunction(method));
|
|
1210
|
+
}
|
|
1211
|
+
if (methods.length === 0)
|
|
1212
|
+
continue;
|
|
1213
|
+
const fields = [];
|
|
1214
|
+
for (const prop of cls.getProperties()) {
|
|
1215
|
+
fields.push({ name: prop.getName(), tsType: typeToString(prop.getType()) });
|
|
1216
|
+
}
|
|
1217
|
+
classes.push({ name: cls.getName() ?? "Anonymous", fields, methods });
|
|
1218
|
+
}
|
|
420
1219
|
return {
|
|
421
1220
|
file: sourceFile.getFilePath(),
|
|
422
1221
|
typeDecls,
|
|
423
|
-
|
|
1222
|
+
constants,
|
|
1223
|
+
functions,
|
|
1224
|
+
classes,
|
|
424
1225
|
};
|
|
425
1226
|
}
|
|
426
1227
|
// ── Main ─────────────────────────────────────────────────────
|
|
@@ -430,6 +1231,6 @@ if (process.argv[1]?.match(/extract\.(ts|js)$/)) {
|
|
|
430
1231
|
console.error("Usage: extract <file.ts>");
|
|
431
1232
|
process.exit(1);
|
|
432
1233
|
}
|
|
433
|
-
const proj = new Project({ compilerOptions: { strict: true } });
|
|
1234
|
+
const proj = new Project({ compilerOptions: { strict: true, target: ScriptTarget.ESNext, lib: ["lib.esnext.d.ts"] } });
|
|
434
1235
|
console.log(JSON.stringify(extractModule(proj.addSourceFileAtPath(file)), null, 2));
|
|
435
1236
|
}
|