lemmascript 0.2.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 +3 -2
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +1 -1
- package/tools/dist/dafny-emit.js +149 -36
- package/tools/dist/extract.js +480 -50
- package/tools/dist/lean-emit.js +6 -2
- package/tools/dist/lsc.js +21 -4
- package/tools/dist/resolve.js +225 -25
- package/tools/dist/specparser.js +5 -2
- package/tools/dist/transform.js +284 -19
- package/tools/dist/types.js +14 -1
package/tools/dist/extract.js
CHANGED
|
@@ -8,6 +8,21 @@ import { Project, Node, SyntaxKind, ScriptTarget } from "ts-morph";
|
|
|
8
8
|
// ── Expression extraction ────────────────────────────────────
|
|
9
9
|
/** When set, calls whose function/method name matches this key are replaced with havoc. */
|
|
10
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
|
+
}
|
|
11
26
|
function extractExpr(node) {
|
|
12
27
|
// Havoc key matching: replace matching calls with havoc expression
|
|
13
28
|
if (_havocKey && Node.isCallExpression(node)) {
|
|
@@ -63,9 +78,24 @@ function extractExpr(node) {
|
|
|
63
78
|
if (node.getKind() === SyntaxKind.ThisKeyword) {
|
|
64
79
|
return { kind: "var", name: "this" };
|
|
65
80
|
}
|
|
66
|
-
// Property access: x.foo
|
|
81
|
+
// Property access: x.foo or x?.foo
|
|
67
82
|
if (Node.isPropertyAccessExpression(node)) {
|
|
68
|
-
|
|
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 };
|
|
69
99
|
}
|
|
70
100
|
// Element access: arr[i]
|
|
71
101
|
if (Node.isElementAccessExpression(node)) {
|
|
@@ -129,15 +159,38 @@ function extractExpr(node) {
|
|
|
129
159
|
}
|
|
130
160
|
throw new Error(`Unsupported arrow function body: ${node.getText().slice(0, 80)}`);
|
|
131
161
|
}
|
|
132
|
-
// Array literal: [a, b, c] → arrayLiteral,
|
|
162
|
+
// Array literal: [a, b, c] → arrayLiteral, with spreads → concatenation
|
|
133
163
|
if (Node.isArrayLiteralExpression(node)) {
|
|
134
164
|
const elems = node.getElements();
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
return { kind: "
|
|
165
|
+
const hasSpread = elems.some(e => Node.isSpreadElement(e));
|
|
166
|
+
if (!hasSpread) {
|
|
167
|
+
return { kind: "arrayLiteral", elems: elems.map(e => extractExpr(e)) };
|
|
168
|
+
}
|
|
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
|
+
}
|
|
138
184
|
}
|
|
139
|
-
|
|
140
|
-
|
|
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;
|
|
141
194
|
}
|
|
142
195
|
// Object literal: { res: true, done: false } or { ...obj, res: true }
|
|
143
196
|
if (Node.isObjectLiteralExpression(node)) {
|
|
@@ -172,14 +225,31 @@ function extractExpr(node) {
|
|
|
172
225
|
const name = node.getExpression().getText();
|
|
173
226
|
if (name === "Map" || name === "Set") {
|
|
174
227
|
const typeArgs = node.getTypeArguments();
|
|
228
|
+
// Use explicit type args if present, otherwise infer from TS type system
|
|
175
229
|
const tsType = typeArgs && typeArgs.length > 0
|
|
176
230
|
? `${name}<${typeArgs.map(t => t.getText()).join(", ")}>`
|
|
177
|
-
:
|
|
231
|
+
: _eraseGenerics(typeToString(node.getType()));
|
|
178
232
|
const args = node.getArguments();
|
|
179
|
-
// new Map(
|
|
233
|
+
// new Map(source) — clone existing map or build from entries
|
|
180
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
|
|
181
242
|
return { kind: "call", fn: { kind: "var", name: "__mapFromArray" }, args: [extractExpr(args[0])] };
|
|
182
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
|
+
}
|
|
183
253
|
return { kind: "emptyCollection", collectionType: name, tsType };
|
|
184
254
|
}
|
|
185
255
|
}
|
|
@@ -187,6 +257,10 @@ function extractExpr(node) {
|
|
|
187
257
|
if (Node.isAsExpression(node)) {
|
|
188
258
|
return extractExpr(node.getExpression());
|
|
189
259
|
}
|
|
260
|
+
// null → undefined (both map to None in backends)
|
|
261
|
+
if (Node.isNullLiteral(node)) {
|
|
262
|
+
return { kind: "var", name: "undefined" };
|
|
263
|
+
}
|
|
190
264
|
throw new Error(`Unsupported expression: ${node.getText()}`);
|
|
191
265
|
}
|
|
192
266
|
// ── Annotation parsing ───────────────────────────────────────
|
|
@@ -216,13 +290,15 @@ function collectAnnotations(node, body) {
|
|
|
216
290
|
return own;
|
|
217
291
|
}
|
|
218
292
|
// ── Type declaration extraction ──────────────────────────────
|
|
219
|
-
function extractTypeDecl(decl) {
|
|
293
|
+
function extractTypeDecl(decl, extraDecls) {
|
|
220
294
|
const name = decl.getName();
|
|
221
295
|
const type = decl.getType();
|
|
296
|
+
const typeParams = decl.getTypeParameters().map(tp => tp.getName());
|
|
297
|
+
const tpField = typeParams.length > 0 ? typeParams : undefined;
|
|
222
298
|
if (type.isUnion()) {
|
|
223
299
|
const members = type.getUnionTypes();
|
|
224
300
|
if (members.every(m => m.isStringLiteral())) {
|
|
225
|
-
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()) };
|
|
226
302
|
}
|
|
227
303
|
if (members.every(m => m.isObject())) {
|
|
228
304
|
const discriminant = findDiscriminant(members);
|
|
@@ -230,7 +306,8 @@ function extractTypeDecl(decl) {
|
|
|
230
306
|
const variants = members.map(m => {
|
|
231
307
|
const tagProp = m.getProperty(discriminant);
|
|
232
308
|
const tagType = tagProp?.getTypeAtLocation(decl);
|
|
233
|
-
const tag = tagType?.getLiteralValue()
|
|
309
|
+
const tag = tagType?.isStringLiteral() ? String(tagType.getLiteralValue())
|
|
310
|
+
: tagType?.getText() ?? "unknown";
|
|
234
311
|
const fields = [];
|
|
235
312
|
for (const prop of m.getProperties()) {
|
|
236
313
|
if (prop.getName() === discriminant)
|
|
@@ -239,15 +316,19 @@ function extractTypeDecl(decl) {
|
|
|
239
316
|
}
|
|
240
317
|
return { name: tag, fields };
|
|
241
318
|
});
|
|
242
|
-
return { name, kind: "discriminated-union", discriminant, variants };
|
|
319
|
+
return { name, typeParams: tpField, kind: "discriminated-union", discriminant, variants };
|
|
243
320
|
}
|
|
244
321
|
}
|
|
245
322
|
}
|
|
246
|
-
if (type.isObject())
|
|
247
|
-
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 };
|
|
248
329
|
return null;
|
|
249
330
|
}
|
|
250
|
-
function extractInterface(decl) {
|
|
331
|
+
function extractInterface(decl, extraDecls) {
|
|
251
332
|
// Collect field type overrides from trailing //@ type annotations
|
|
252
333
|
const overrides = new Map();
|
|
253
334
|
for (const member of decl.getMembers()) {
|
|
@@ -258,16 +339,44 @@ function extractInterface(decl) {
|
|
|
258
339
|
overrides.set(member.getName(), match[1]);
|
|
259
340
|
}
|
|
260
341
|
}
|
|
261
|
-
return extractRecord(decl.getName(), decl.getType(), decl, overrides);
|
|
342
|
+
return extractRecord(decl.getName(), decl.getType(), decl, overrides, extraDecls);
|
|
262
343
|
}
|
|
263
|
-
function extractRecord(name, type, locationNode, overrides) {
|
|
344
|
+
function extractRecord(name, type, locationNode, overrides, extraDecls) {
|
|
264
345
|
const props = type.getProperties();
|
|
265
346
|
if (props.length === 0)
|
|
266
347
|
return null;
|
|
267
348
|
const fields = [];
|
|
268
349
|
for (const prop of props) {
|
|
269
350
|
const override = overrides?.get(prop.getName());
|
|
270
|
-
|
|
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
|
+
}
|
|
271
380
|
fields.push({ name: prop.getName(), tsType });
|
|
272
381
|
}
|
|
273
382
|
return { name, kind: "record", fields };
|
|
@@ -283,7 +392,7 @@ function findDiscriminant(members) {
|
|
|
283
392
|
if (!p)
|
|
284
393
|
return false;
|
|
285
394
|
const t = p.getDeclarations()[0] ? p.getTypeAtLocation(p.getDeclarations()[0]) : null;
|
|
286
|
-
return t?.isStringLiteral() ?? false;
|
|
395
|
+
return (t?.isStringLiteral() || t?.isBooleanLiteral()) ?? false;
|
|
287
396
|
});
|
|
288
397
|
if (allHave)
|
|
289
398
|
return name;
|
|
@@ -310,7 +419,11 @@ function typeToString(type) {
|
|
|
310
419
|
return name;
|
|
311
420
|
}
|
|
312
421
|
if (type.isUnion()) {
|
|
313
|
-
|
|
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(", ")}]`;
|
|
314
427
|
}
|
|
315
428
|
if (type.isArray()) {
|
|
316
429
|
const elem = type.getArrayElementTypeOrThrow();
|
|
@@ -366,18 +479,44 @@ function extractStmts(stmts) {
|
|
|
366
479
|
for (const s of stmts) {
|
|
367
480
|
const line = s.getStartLineNumber();
|
|
368
481
|
// Ghost annotations from leading comments → inject before this statement
|
|
369
|
-
|
|
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
|
+
}
|
|
370
488
|
if (Node.isVariableStatement(s)) {
|
|
371
489
|
const havocMatch = s.getLeadingCommentRanges()
|
|
372
|
-
.map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s+(\S+))?$/))
|
|
490
|
+
.map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s*:\s*(.+)|(?:\s+(\S+)))?$/))
|
|
373
491
|
.find(m => m !== null);
|
|
374
|
-
const
|
|
492
|
+
const havocType = havocMatch?.[1]?.trim() ?? null; // //@ havoc : Type
|
|
493
|
+
const havocKey = havocMatch?.[2] ?? null; // //@ havoc key
|
|
375
494
|
const isHavoc = !!havocMatch;
|
|
376
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
|
+
}
|
|
377
514
|
const declType = d.getType();
|
|
378
515
|
let init;
|
|
379
516
|
if (isHavoc && !havocKey) {
|
|
380
|
-
|
|
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) };
|
|
381
520
|
}
|
|
382
521
|
else {
|
|
383
522
|
const initializer = d.getInitializer();
|
|
@@ -389,7 +528,7 @@ function extractStmts(stmts) {
|
|
|
389
528
|
kind: "let",
|
|
390
529
|
name: d.getName(),
|
|
391
530
|
mutable: s.getDeclarationKind() === "let",
|
|
392
|
-
tsType: typeToString(declType),
|
|
531
|
+
tsType: havocType ?? _eraseGenerics(typeToString(declType)),
|
|
393
532
|
init,
|
|
394
533
|
line,
|
|
395
534
|
});
|
|
@@ -535,6 +674,11 @@ function extractStmts(stmts) {
|
|
|
535
674
|
result.push({ kind: "throw", line });
|
|
536
675
|
continue;
|
|
537
676
|
}
|
|
677
|
+
// Block statement: { ... } — flatten into parent
|
|
678
|
+
if (Node.isBlock(s)) {
|
|
679
|
+
result.push(...extractStmts(s.getStatements()));
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
538
682
|
throw new Error(`Unsupported statement at line ${line}: ${s.getText().slice(0, 80)}`);
|
|
539
683
|
}
|
|
540
684
|
// Ghost comments after the last statement (before closing brace) appear as sibling trivia nodes
|
|
@@ -568,12 +712,35 @@ function extractStmts(stmts) {
|
|
|
568
712
|
return result;
|
|
569
713
|
}
|
|
570
714
|
// ── Function extraction ──────────────────────────────────────
|
|
571
|
-
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
|
+
}
|
|
572
727
|
const body = fn.getBody();
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
+
}
|
|
577
744
|
const typeAnnotations = [];
|
|
578
745
|
for (const a of annots) {
|
|
579
746
|
if (a.kind === "type") {
|
|
@@ -584,27 +751,94 @@ function extractFunction(fn) {
|
|
|
584
751
|
}
|
|
585
752
|
return {
|
|
586
753
|
name: fn.getName?.() ?? "<anonymous>",
|
|
587
|
-
|
|
588
|
-
|
|
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
|
+
})(),
|
|
589
781
|
requires: annots.filter(a => a.kind === "requires").map(a => a.expr),
|
|
590
782
|
ensures: annots.filter(a => a.kind === "ensures").map(a => a.expr),
|
|
591
783
|
typeAnnotations,
|
|
592
|
-
body:
|
|
784
|
+
body: extractedBody,
|
|
593
785
|
line: fn.getStartLineNumber(),
|
|
594
786
|
};
|
|
595
787
|
}
|
|
596
788
|
// ── Module extraction ────────────────────────────────────────
|
|
597
789
|
export function extractModule(sourceFile) {
|
|
598
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
|
+
}
|
|
599
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));
|
|
600
829
|
for (const stmt of sourceFile.getStatements()) {
|
|
601
|
-
if (Node.isTypeAliasDeclaration(stmt)) {
|
|
602
|
-
const
|
|
830
|
+
if (Node.isTypeAliasDeclaration(stmt) && !declaredNames.has(stmt.getName())) {
|
|
831
|
+
const extra = [];
|
|
832
|
+
const info = extractTypeDecl(stmt, extra);
|
|
833
|
+
typeDecls.push(...extra);
|
|
603
834
|
if (info)
|
|
604
835
|
typeDecls.push(info);
|
|
605
836
|
}
|
|
606
|
-
else if (Node.isInterfaceDeclaration(stmt)) {
|
|
607
|
-
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);
|
|
608
842
|
if (info)
|
|
609
843
|
typeDecls.push(info);
|
|
610
844
|
}
|
|
@@ -640,35 +874,134 @@ export function extractModule(sourceFile) {
|
|
|
640
874
|
for (const fn of sourceFile.getFunctions()) {
|
|
641
875
|
allFns.push({ name: fn.getName() ?? "<anonymous>", node: fn });
|
|
642
876
|
}
|
|
643
|
-
// const f = (...) =>
|
|
877
|
+
// const f = (...) => expr OR const f = (...) => { ... }
|
|
644
878
|
for (const stmt of sourceFile.getStatements()) {
|
|
645
879
|
if (Node.isVariableStatement(stmt)) {
|
|
646
880
|
for (const decl of stmt.getDeclarationList().getDeclarations()) {
|
|
647
881
|
const init = decl.getInitializer();
|
|
648
882
|
if (init && Node.isArrowFunction(init)) {
|
|
649
|
-
allFns.push({ name: decl.getName(), node: init });
|
|
883
|
+
allFns.push({ name: decl.getName(), node: init, parentStmt: stmt });
|
|
650
884
|
}
|
|
651
885
|
}
|
|
652
886
|
}
|
|
653
887
|
}
|
|
654
888
|
// If any function has //@ verify, only extract those (brownfield mode).
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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;
|
|
660
903
|
const functions = fnsToExtract.map(f => {
|
|
661
|
-
|
|
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);
|
|
662
907
|
raw.name = f.name; // use the const name, not "<anonymous>"
|
|
663
908
|
return raw;
|
|
664
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
|
+
}
|
|
665
998
|
// 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
999
|
if (hasVerifyDirective) {
|
|
668
1000
|
const referencedNames = new Set();
|
|
669
1001
|
function collectNames(stmts) {
|
|
670
1002
|
for (const s of stmts) {
|
|
671
1003
|
if (s.kind === "let") {
|
|
1004
|
+
referencedNames.add(s.tsType);
|
|
672
1005
|
collectNamesExpr(s.init);
|
|
673
1006
|
}
|
|
674
1007
|
if (s.kind === "assign") {
|
|
@@ -743,6 +1076,26 @@ export function extractModule(sourceFile) {
|
|
|
743
1076
|
}
|
|
744
1077
|
}
|
|
745
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)));
|
|
746
1099
|
}
|
|
747
1100
|
// Resolve imported types: extract types referenced in function signatures but not in this file
|
|
748
1101
|
const knownTypes = new Set(typeDecls.map(d => d.name));
|
|
@@ -753,15 +1106,53 @@ export function extractModule(sourceFile) {
|
|
|
753
1106
|
resolveType(t.getArrayElementTypeOrThrow(), locationNode);
|
|
754
1107
|
return;
|
|
755
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
|
+
}
|
|
756
1141
|
for (const arg of t.getTypeArguments())
|
|
757
1142
|
resolveType(arg, locationNode);
|
|
758
1143
|
const sym = t.getSymbol() ?? t.getAliasSymbol();
|
|
759
1144
|
const name = sym?.getName();
|
|
760
|
-
if (name && !knownTypes.has(name) && !builtins.has(name) && t.isObject()) {
|
|
761
|
-
const
|
|
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);
|
|
762
1148
|
if (info) {
|
|
1149
|
+
typeDecls.push(...extra);
|
|
763
1150
|
typeDecls.push(info);
|
|
764
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
|
+
}
|
|
765
1156
|
}
|
|
766
1157
|
}
|
|
767
1158
|
}
|
|
@@ -769,6 +1160,45 @@ export function extractModule(sourceFile) {
|
|
|
769
1160
|
for (const p of f.node.getParameters())
|
|
770
1161
|
resolveType(p.getType(), p);
|
|
771
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
|
+
}
|
|
772
1202
|
// Extract classes with //@ verify methods
|
|
773
1203
|
const classes = [];
|
|
774
1204
|
for (const cls of sourceFile.getClasses()) {
|