lemmascript 0.1.0 → 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/README.md +24 -31
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +46 -52
- package/tools/dist/dafny-emit.js +362 -73
- package/tools/dist/extract.js +386 -15
- package/tools/dist/ir.js +2 -2
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +393 -0
- package/tools/dist/lsc.js +44 -43
- package/tools/dist/resolve.js +285 -19
- package/tools/dist/specparser.js +61 -7
- package/tools/dist/transform.js +533 -187
- package/tools/dist/types.js +46 -13
package/tools/dist/extract.js
CHANGED
|
@@ -4,13 +4,48 @@
|
|
|
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;
|
|
9
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
|
+
}
|
|
10
22
|
// Numeric literal
|
|
11
23
|
if (Node.isNumericLiteral(node)) {
|
|
12
24
|
return { kind: "num", value: Number(node.getLiteralValue()) };
|
|
13
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
|
+
}
|
|
14
49
|
// String literal
|
|
15
50
|
if (Node.isStringLiteral(node)) {
|
|
16
51
|
return { kind: "str", value: node.getLiteralValue() };
|
|
@@ -24,6 +59,10 @@ function extractExpr(node) {
|
|
|
24
59
|
if (Node.isIdentifier(node)) {
|
|
25
60
|
return { kind: "var", name: node.getText() };
|
|
26
61
|
}
|
|
62
|
+
// this
|
|
63
|
+
if (node.getKind() === SyntaxKind.ThisKeyword) {
|
|
64
|
+
return { kind: "var", name: "this" };
|
|
65
|
+
}
|
|
27
66
|
// Property access: x.foo
|
|
28
67
|
if (Node.isPropertyAccessExpression(node)) {
|
|
29
68
|
return { kind: "field", obj: extractExpr(node.getExpression()), field: node.getName() };
|
|
@@ -108,6 +147,10 @@ function extractExpr(node) {
|
|
|
108
147
|
if (Node.isSpreadAssignment(prop)) {
|
|
109
148
|
spread = extractExpr(prop.getExpression());
|
|
110
149
|
}
|
|
150
|
+
else if (Node.isShorthandPropertyAssignment(prop)) {
|
|
151
|
+
const name = prop.getName();
|
|
152
|
+
fields.push({ name, value: { kind: "var", name } });
|
|
153
|
+
}
|
|
111
154
|
else if (Node.isPropertyAssignment(prop)) {
|
|
112
155
|
const init = prop.getInitializer();
|
|
113
156
|
if (init)
|
|
@@ -120,6 +163,30 @@ function extractExpr(node) {
|
|
|
120
163
|
if (Node.isConditionalExpression(node)) {
|
|
121
164
|
return { kind: "conditional", cond: extractExpr(node.getCondition()), then: extractExpr(node.getWhenTrue()), else: extractExpr(node.getWhenFalse()) };
|
|
122
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
|
+
}
|
|
123
190
|
throw new Error(`Unsupported expression: ${node.getText()}`);
|
|
124
191
|
}
|
|
125
192
|
// ── Annotation parsing ───────────────────────────────────────
|
|
@@ -224,39 +291,106 @@ function findDiscriminant(members) {
|
|
|
224
291
|
return null;
|
|
225
292
|
}
|
|
226
293
|
function typeToString(type) {
|
|
227
|
-
if (type.
|
|
294
|
+
if (type.isUndefined())
|
|
295
|
+
return "undefined";
|
|
296
|
+
if (type.isNumber() || type.isNumberLiteral())
|
|
228
297
|
return "number";
|
|
229
|
-
if (type.
|
|
298
|
+
if (type.isBigInt() || type.isBigIntLiteral())
|
|
299
|
+
return "bigint";
|
|
300
|
+
if (type.isString() || type.isStringLiteral())
|
|
230
301
|
return "string";
|
|
231
302
|
if (type.isBoolean())
|
|
232
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
|
+
}
|
|
233
315
|
if (type.isArray()) {
|
|
234
316
|
const elem = type.getArrayElementTypeOrThrow();
|
|
235
317
|
return `${typeToString(elem)}[]`;
|
|
236
318
|
}
|
|
237
319
|
const symbol = type.getSymbol() ?? type.getAliasSymbol();
|
|
238
|
-
if (symbol)
|
|
239
|
-
|
|
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
|
+
}
|
|
240
328
|
return type.getText();
|
|
241
329
|
}
|
|
242
330
|
const COMPOUND_OPS = {
|
|
243
331
|
"+=": "+", "-=": "-", "*=": "*", "/=": "/", "%=": "%",
|
|
244
332
|
};
|
|
245
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
|
+
}
|
|
246
364
|
function extractStmts(stmts) {
|
|
247
365
|
const result = [];
|
|
248
366
|
for (const s of stmts) {
|
|
249
367
|
const line = s.getStartLineNumber();
|
|
368
|
+
// Ghost annotations from leading comments → inject before this statement
|
|
369
|
+
result.push(...parseSpecComments(s.getLeadingCommentRanges(), line));
|
|
250
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;
|
|
251
376
|
for (const d of s.getDeclarations()) {
|
|
252
377
|
const declType = d.getType();
|
|
253
|
-
|
|
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
|
+
}
|
|
254
388
|
result.push({
|
|
255
389
|
kind: "let",
|
|
256
390
|
name: d.getName(),
|
|
257
391
|
mutable: s.getDeclarationKind() === "let",
|
|
258
392
|
tsType: typeToString(declType),
|
|
259
|
-
init
|
|
393
|
+
init,
|
|
260
394
|
line,
|
|
261
395
|
});
|
|
262
396
|
}
|
|
@@ -279,13 +413,29 @@ function extractStmts(stmts) {
|
|
|
279
413
|
}
|
|
280
414
|
if (Node.isForOfStatement(s)) {
|
|
281
415
|
const init = s.getInitializer();
|
|
282
|
-
const
|
|
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
|
+
}
|
|
283
433
|
const bodyNode = s.getStatement();
|
|
284
434
|
const bodyStmts = Node.isBlock(bodyNode) ? bodyNode.getStatements() : [bodyNode];
|
|
285
435
|
const annots = collectAnnotations(s, bodyStmts);
|
|
286
436
|
result.push({
|
|
287
437
|
kind: "forof",
|
|
288
|
-
|
|
438
|
+
names,
|
|
289
439
|
iterable: extractExpr(s.getExpression()),
|
|
290
440
|
invariants: annots.filter(a => a.kind === "invariant").map(a => a.expr),
|
|
291
441
|
doneWith: annots.find(a => a.kind === "done_with")?.expr ?? null,
|
|
@@ -344,8 +494,18 @@ function extractStmts(stmts) {
|
|
|
344
494
|
}
|
|
345
495
|
if (Node.isExpressionStatement(s)) {
|
|
346
496
|
const expr = s.getExpression();
|
|
347
|
-
//
|
|
348
|
-
if (Node.isBinaryExpression(expr) && expr.getOperatorToken().getText() === "=") {
|
|
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() === "=") {
|
|
349
509
|
result.push({ kind: "assign", target: expr.getLeft().getText(), value: extractExpr(expr.getRight()), line });
|
|
350
510
|
// x += e, x -= e, etc.
|
|
351
511
|
}
|
|
@@ -371,15 +531,47 @@ function extractStmts(stmts) {
|
|
|
371
531
|
}
|
|
372
532
|
continue;
|
|
373
533
|
}
|
|
534
|
+
if (Node.isThrowStatement(s)) {
|
|
535
|
+
result.push({ kind: "throw", line });
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
374
538
|
throw new Error(`Unsupported statement at line ${line}: ${s.getText().slice(0, 80)}`);
|
|
375
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
|
+
}
|
|
376
568
|
return result;
|
|
377
569
|
}
|
|
378
570
|
// ── Function extraction ──────────────────────────────────────
|
|
379
571
|
function extractFunction(fn) {
|
|
380
572
|
const body = fn.getBody();
|
|
381
573
|
if (!body || !Node.isBlock(body))
|
|
382
|
-
throw new Error(`${fn.getName()}: function body is not a block`);
|
|
574
|
+
throw new Error(`${fn.getName?.() ?? "arrow"}: function body is not a block`);
|
|
383
575
|
const bodyStmts = body.getStatements();
|
|
384
576
|
const annots = collectAnnotations(fn, bodyStmts);
|
|
385
577
|
const typeAnnotations = [];
|
|
@@ -391,7 +583,7 @@ function extractFunction(fn) {
|
|
|
391
583
|
}
|
|
392
584
|
}
|
|
393
585
|
return {
|
|
394
|
-
name: fn.getName() ?? "<anonymous>",
|
|
586
|
+
name: fn.getName?.() ?? "<anonymous>",
|
|
395
587
|
params: fn.getParameters().map(p => ({ name: p.getName(), tsType: p.getTypeNode()?.getText() ?? "unknown" })),
|
|
396
588
|
returnType: fn.getReturnTypeNode()?.getText() ?? "unknown",
|
|
397
589
|
requires: annots.filter(a => a.kind === "requires").map(a => a.expr),
|
|
@@ -417,10 +609,189 @@ export function extractModule(sourceFile) {
|
|
|
417
609
|
typeDecls.push(info);
|
|
418
610
|
}
|
|
419
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
|
+
}
|
|
420
789
|
return {
|
|
421
790
|
file: sourceFile.getFilePath(),
|
|
422
791
|
typeDecls,
|
|
423
|
-
|
|
792
|
+
constants,
|
|
793
|
+
functions,
|
|
794
|
+
classes,
|
|
424
795
|
};
|
|
425
796
|
}
|
|
426
797
|
// ── Main ─────────────────────────────────────────────────────
|
|
@@ -430,6 +801,6 @@ if (process.argv[1]?.match(/extract\.(ts|js)$/)) {
|
|
|
430
801
|
console.error("Usage: extract <file.ts>");
|
|
431
802
|
process.exit(1);
|
|
432
803
|
}
|
|
433
|
-
const proj = new Project({ compilerOptions: { strict: true } });
|
|
804
|
+
const proj = new Project({ compilerOptions: { strict: true, target: ScriptTarget.ESNext, lib: ["lib.esnext.d.ts"] } });
|
|
434
805
|
console.log(JSON.stringify(extractModule(proj.addSourceFileAtPath(file)), null, 2));
|
|
435
806
|
}
|
package/tools/dist/ir.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* IR — the intermediate representation between transform and emit.
|
|
3
3
|
*
|
|
4
4
|
* The transform phase produces these types.
|
|
5
|
-
* The emit phase pretty-prints them to Lean
|
|
5
|
+
* The emit phase pretty-prints them to backend syntax (Lean or Dafny).
|
|
6
6
|
*/
|
|
7
7
|
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lean backend commands: gen, check.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, writeFileSync } from "fs";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import path from "path";
|
|
7
|
+
export function leanGen(typesPath, defPath, typesText, defText) {
|
|
8
|
+
if (typesPath && typesText) {
|
|
9
|
+
writeFileSync(typesPath, typesText);
|
|
10
|
+
console.log(`Generated: ${typesPath}`);
|
|
11
|
+
}
|
|
12
|
+
writeFileSync(defPath, defText);
|
|
13
|
+
console.log(`Generated: ${defPath}`);
|
|
14
|
+
}
|
|
15
|
+
export function leanCheck(dir, base) {
|
|
16
|
+
let lakeDir = dir;
|
|
17
|
+
while (lakeDir !== path.dirname(lakeDir)) {
|
|
18
|
+
if (existsSync(path.join(lakeDir, "lakefile.lean")))
|
|
19
|
+
break;
|
|
20
|
+
lakeDir = path.dirname(lakeDir);
|
|
21
|
+
}
|
|
22
|
+
const proofPath = path.join(dir, `${base}.proof.lean`);
|
|
23
|
+
if (!existsSync(proofPath)) {
|
|
24
|
+
console.error(`No proof file: ${proofPath}`);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
console.log("Running lake build...");
|
|
28
|
+
try {
|
|
29
|
+
execSync(`lake build`, { cwd: lakeDir, stdio: "inherit" });
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|