lemmascript 0.5.18 → 0.5.20
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 +1 -1
- package/package.json +1 -1
- package/tools/dist/autohavoc.js +2 -0
- package/tools/dist/builtins.js +125 -0
- package/tools/dist/condition-facts.js +364 -0
- package/tools/dist/dafny-emit.js +228 -37
- package/tools/dist/extract.js +175 -37
- package/tools/dist/info-command.js +68 -0
- package/tools/dist/ir.js +27 -7
- package/tools/dist/lean-emit.js +29 -18
- package/tools/dist/lsc.js +53 -5
- package/tools/dist/names.js +10 -6
- package/tools/dist/narrow.js +296 -677
- package/tools/dist/peephole.js +12 -94
- package/tools/dist/rawir.js +15 -1
- package/tools/dist/resolve.js +268 -249
- package/tools/dist/specparser.js +21 -17
- package/tools/dist/transform.js +411 -131
- package/tools/dist/typedecls.js +59 -0
package/tools/dist/extract.js
CHANGED
|
@@ -6,15 +6,43 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Project, Node, SyntaxKind, ScriptTarget, ts } from "ts-morph";
|
|
8
8
|
import { initTypeParser } from "./types.js";
|
|
9
|
+
import { normalizeBigIntLiteral } from "./rawir.js";
|
|
9
10
|
import { setUserNames, freshName } from "./names.js";
|
|
10
11
|
// ── Expression extraction ────────────────────────────────────
|
|
11
12
|
/** When set, calls whose function/method name matches this key are replaced with havoc. */
|
|
12
13
|
let _havocKey = null;
|
|
14
|
+
/** A leading `//@ havoc`, `//@ havoc : Type`, or `//@ havoc <key>` directive. */
|
|
15
|
+
function havocDirective(s) {
|
|
16
|
+
const m = s.getLeadingCommentRanges()
|
|
17
|
+
.map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s*:\s*(.+)|(?:\s+(\S+)))?$/))
|
|
18
|
+
.find(m => m !== null);
|
|
19
|
+
return m ? { type: m[1]?.trim() ?? null, key: m[2] ?? null } : null;
|
|
20
|
+
}
|
|
21
|
+
/** Run `fn` with `key` as the subexpression havoc key, restoring the outer one. */
|
|
22
|
+
function withHavocKey(key, fn) {
|
|
23
|
+
const saved = _havocKey;
|
|
24
|
+
if (key)
|
|
25
|
+
_havocKey = key;
|
|
26
|
+
try {
|
|
27
|
+
return fn();
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
_havocKey = saved;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
13
33
|
/** Auto-detected cross-file calls. Populated by `extractExpr` whenever it sees
|
|
14
34
|
* a call `Obj.method(...)` or `foo(...)` whose ts-morph symbol resolves to a
|
|
15
35
|
* different `.ts` source file. Emitted in Dafny as `function {:axiom} <flat>`.
|
|
16
36
|
* Cleared at the start of every `extractModule`. */
|
|
17
37
|
const _externs = new Map();
|
|
38
|
+
/** Signature types of *kept* externs, for the imported-type resolver: a type
|
|
39
|
+
* reachable ONLY through an imported function's signature (e.g. an extern
|
|
40
|
+
* returning PresentFact that no local signature mentions) must still be
|
|
41
|
+
* resolved into a full decl, or it synthesizes opaque. Filled only after the
|
|
42
|
+
* extern survives the `_externs` dedup — a cross-file signature superseded by a
|
|
43
|
+
* same-file `//@ extern` contributes nothing to the output, so resolving its
|
|
44
|
+
* types would emit decls that no emitted declaration mentions. */
|
|
45
|
+
const _externSigTypes = [];
|
|
18
46
|
let _currentSourceFile = null;
|
|
19
47
|
/** True only while extracting a function body. Module-level constants that
|
|
20
48
|
* reference cross-file callees (e.g., `BusEvent.define(...)` inside a
|
|
@@ -30,10 +58,14 @@ let _destrCounter = 0;
|
|
|
30
58
|
* lifted `requires`/`ensures` see all the symbols they reference). Idempotent
|
|
31
59
|
* via the `_externs` dedup. */
|
|
32
60
|
function registerExternIfCrossFile(callee, sourceFile) {
|
|
33
|
-
const
|
|
61
|
+
const sigTypes = [];
|
|
62
|
+
const ext = detectCrossFileExtern(callee, sourceFile, sigTypes);
|
|
34
63
|
if (!ext || _externs.has(ext.qualified))
|
|
35
64
|
return;
|
|
36
65
|
_externs.set(ext.qualified, ext);
|
|
66
|
+
// Only now that the extern is kept do its signature types become resolver
|
|
67
|
+
// seeds — see `_externSigTypes`.
|
|
68
|
+
_externSigTypes.push(...sigTypes);
|
|
37
69
|
// Recurse: scan the source decl's body for nested cross-file calls so any
|
|
38
70
|
// symbol referenced by the copied spec is itself declared in the output.
|
|
39
71
|
let symbol = callee.getSymbol();
|
|
@@ -56,7 +88,7 @@ function registerExternIfCrossFile(callee, sourceFile) {
|
|
|
56
88
|
}
|
|
57
89
|
}
|
|
58
90
|
}
|
|
59
|
-
function detectCrossFileExtern(callee, sourceFile) {
|
|
91
|
+
function detectCrossFileExtern(callee, sourceFile, sigTypesOut) {
|
|
60
92
|
let symbol = callee.getSymbol();
|
|
61
93
|
if (!symbol)
|
|
62
94
|
return null;
|
|
@@ -92,12 +124,32 @@ function detectCrossFileExtern(callee, sourceFile) {
|
|
|
92
124
|
// kept): a bare `TMsg`, not `import("/abs/path/transcript").TMsg` — the
|
|
93
125
|
// importing module declares the datatype locally, so the axiom must use the
|
|
94
126
|
// local name.
|
|
95
|
-
|
|
127
|
+
// NoTruncation: a wide expansion (e.g. an alias for a large string-literal
|
|
128
|
+
// union, not importable at the call site) must print whole — a truncated
|
|
129
|
+
// union is unparseable and synthesizes an opaque decl named by its own text.
|
|
130
|
+
const externTypeText = (t) => t.getText(callee, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | ts.TypeFormatFlags.NoTruncation);
|
|
96
131
|
const params = sig.getParameters().map(p => ({
|
|
97
132
|
name: p.getName(),
|
|
98
133
|
tsType: externTypeText(p.getTypeAtLocation(callee)),
|
|
99
134
|
}));
|
|
100
135
|
const returnType = externTypeText(sig.getReturnType());
|
|
136
|
+
for (const p of sig.getParameters()) {
|
|
137
|
+
sigTypesOut.push({ type: p.getTypeAtLocation(callee), node: callee });
|
|
138
|
+
}
|
|
139
|
+
sigTypesOut.push({ type: sig.getReturnType(), node: callee });
|
|
140
|
+
// Also seed from the source declaration's syntactic type nodes: symbol-based
|
|
141
|
+
// types can drop the alias symbol (the printed signature then names an alias
|
|
142
|
+
// like `TypeDecls` that would otherwise synthesize opaque), while a type
|
|
143
|
+
// node's type keeps it.
|
|
144
|
+
const srcDecl = externalDecl;
|
|
145
|
+
for (const sp of srcDecl.getParameters?.() ?? []) {
|
|
146
|
+
const tn = sp.getTypeNode?.();
|
|
147
|
+
if (tn)
|
|
148
|
+
sigTypesOut.push({ type: tn.getType(), node: tn });
|
|
149
|
+
}
|
|
150
|
+
const rtn = srcDecl.getReturnTypeNode?.();
|
|
151
|
+
if (rtn)
|
|
152
|
+
sigTypesOut.push({ type: rtn.getType(), node: rtn });
|
|
101
153
|
let qualified;
|
|
102
154
|
if (Node.isPropertyAccessExpression(callee)) {
|
|
103
155
|
qualified = `${callee.getExpression().getText()}.${callee.getName()}`;
|
|
@@ -293,24 +345,29 @@ function _eraseGenerics(tsType) {
|
|
|
293
345
|
return tsType;
|
|
294
346
|
}
|
|
295
347
|
function extractExpr(node) {
|
|
296
|
-
// Havoc key matching: replace matching calls with havoc expression
|
|
297
|
-
if (_havocKey && Node.isCallExpression(node)) {
|
|
348
|
+
// Havoc key matching: replace matching calls or new expressions with havoc expression
|
|
349
|
+
if (_havocKey && (Node.isCallExpression(node) || Node.isNewExpression(node))) {
|
|
298
350
|
const fnExpr = node.getExpression();
|
|
299
351
|
const name = Node.isPropertyAccessExpression(fnExpr) ? fnExpr.getName()
|
|
300
352
|
: Node.isIdentifier(fnExpr) ? fnExpr.getText()
|
|
301
353
|
: null;
|
|
302
354
|
if (name === _havocKey) {
|
|
303
|
-
|
|
355
|
+
// Prefer the contextual type: the abstracted value stands in where the
|
|
356
|
+
// surrounding code expects it, and a subclass (`new SdkError` into an
|
|
357
|
+
// `Error` field) has no subtyping relation once both are opaque.
|
|
358
|
+
const ty = node.getContextualType() ?? node.getType();
|
|
359
|
+
return { kind: "havoc", tsType: typeToString(ty) };
|
|
304
360
|
}
|
|
305
361
|
}
|
|
306
362
|
// Numeric literal
|
|
307
363
|
if (Node.isNumericLiteral(node)) {
|
|
308
364
|
return { kind: "num", value: Number(node.getLiteralValue()) };
|
|
309
365
|
}
|
|
310
|
-
// BigInt literal (e.g. 32n, 0xffffn) — integer, with bigint division
|
|
366
|
+
// BigInt literal (e.g. 32n, 0xffffn) — exact integer, with bigint division
|
|
367
|
+
// semantics. Kept as a decimal string: `getLiteralValue()`/`Number()` would
|
|
368
|
+
// round anything past 2^53 (`9007199254740993n` → `9007199254740992`).
|
|
311
369
|
if (Node.isBigIntLiteral(node)) {
|
|
312
|
-
|
|
313
|
-
return { kind: "num", value: Number(text), big: true };
|
|
370
|
+
return { kind: "bigint", value: normalizeBigIntLiteral(node.getText()) };
|
|
314
371
|
}
|
|
315
372
|
// Template literal: `foo${x}bar` → "foo" + x + "bar"
|
|
316
373
|
if (Node.isTemplateExpression(node)) {
|
|
@@ -462,10 +519,9 @@ function extractExpr(node) {
|
|
|
462
519
|
const typeNode = p.getTypeNode();
|
|
463
520
|
return { name: p.getName(), tsType: typeNode ? typeNode.getText() : undefined };
|
|
464
521
|
});
|
|
465
|
-
//
|
|
466
|
-
// return-position record literals
|
|
467
|
-
const
|
|
468
|
-
const returnTsType = retNode ? typeToString(node.getReturnType()) : undefined;
|
|
522
|
+
// Return type from the checker — inferred when unannotated — so resolve can
|
|
523
|
+
// type return-position record literals and give the lambda a real fn type.
|
|
524
|
+
const returnTsType = typeToString(node.getReturnType());
|
|
469
525
|
const body = node.getBody();
|
|
470
526
|
if (Node.isExpression(body)) {
|
|
471
527
|
return { kind: "lambda", params, body: extractExpr(body), returnTsType };
|
|
@@ -688,8 +744,14 @@ function extractTypeDecl(decl, extraDecls) {
|
|
|
688
744
|
continue;
|
|
689
745
|
let tsType = typeToString(prop.getTypeAtLocation(decl));
|
|
690
746
|
const propDecl = prop.getDeclarations()[0];
|
|
691
|
-
|
|
692
|
-
|
|
747
|
+
tsType = declaredTypeTextIfBetter(propDecl, tsType);
|
|
748
|
+
if (propDecl && propDecl.hasQuestionToken?.()) {
|
|
749
|
+
// Normalize checker output that puts `undefined` first, then
|
|
750
|
+
// ensure exactly one trailing `| undefined`.
|
|
751
|
+
if (tsType.startsWith("undefined | "))
|
|
752
|
+
tsType = `${tsType.slice("undefined | ".length)} | undefined`;
|
|
753
|
+
else if (!tsType.includes(" | undefined"))
|
|
754
|
+
tsType = `${tsType} | undefined`;
|
|
693
755
|
}
|
|
694
756
|
fields.push({ name: prop.getName(), tsType });
|
|
695
757
|
}
|
|
@@ -780,12 +842,18 @@ function extractRecord(name, type, locationNode, overrides, extraDecls) {
|
|
|
780
842
|
}
|
|
781
843
|
const propType = prop.getTypeAtLocation(locationNode);
|
|
782
844
|
let tsType = typeToString(propType);
|
|
845
|
+
const propDecl = prop.getDeclarations()[0];
|
|
846
|
+
tsType = declaredTypeTextIfBetter(propDecl, tsType);
|
|
783
847
|
// Optional property: `foo?: T` reports as `T` (ts-morph strips the
|
|
784
848
|
// `| undefined` from a question-token type). Add it back so the field
|
|
785
849
|
// resolves to `Optional<T>`.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
850
|
+
if (propDecl && propDecl.hasQuestionToken?.()) {
|
|
851
|
+
// Normalize checker output that puts `undefined` first, then ensure
|
|
852
|
+
// exactly one trailing `| undefined`.
|
|
853
|
+
if (tsType.startsWith("undefined | "))
|
|
854
|
+
tsType = `${tsType.slice("undefined | ".length)} | undefined`;
|
|
855
|
+
else if (!tsType.includes(" | undefined"))
|
|
856
|
+
tsType = `${tsType} | undefined`;
|
|
789
857
|
}
|
|
790
858
|
// Inline anonymous object types: ts-morph names them __type.
|
|
791
859
|
// Generate a synthetic named record and reference it by name instead.
|
|
@@ -832,6 +900,23 @@ function findDiscriminant(members) {
|
|
|
832
900
|
}
|
|
833
901
|
return null;
|
|
834
902
|
}
|
|
903
|
+
/** Recover a field's *declared* type text when the semantic printer degraded
|
|
904
|
+
* to ts-morph's anonymous `__type` — a self-referential alias reached
|
|
905
|
+
* through a `| null` union expands structurally and loses its name. The
|
|
906
|
+
* syntactic node text preserves the alias spelling (`TExpr | null`). Only
|
|
907
|
+
* plain reference text is used: inline object literals (containing `{`)
|
|
908
|
+
* keep the `__type` marker so record synthesis can handle them. */
|
|
909
|
+
function declaredTypeTextIfBetter(propDecl, tsType) {
|
|
910
|
+
if (!tsType.includes("__type") || !propDecl)
|
|
911
|
+
return tsType;
|
|
912
|
+
const tn = propDecl.getTypeNode?.();
|
|
913
|
+
if (!tn)
|
|
914
|
+
return tsType;
|
|
915
|
+
const text = tn.getText();
|
|
916
|
+
if (text.includes("__type") || text.includes("{"))
|
|
917
|
+
return tsType;
|
|
918
|
+
return text;
|
|
919
|
+
}
|
|
835
920
|
function typeToString(type) {
|
|
836
921
|
if (type.isUndefined())
|
|
837
922
|
return "undefined";
|
|
@@ -1031,6 +1116,7 @@ function renameRawExpr(e, from, to) {
|
|
|
1031
1116
|
switch (e.kind) {
|
|
1032
1117
|
case "var": return e.name === from ? { kind: "var", name: to } : e;
|
|
1033
1118
|
case "num":
|
|
1119
|
+
case "bigint":
|
|
1034
1120
|
case "str":
|
|
1035
1121
|
case "bool":
|
|
1036
1122
|
case "result":
|
|
@@ -1179,12 +1265,10 @@ function extractStmts(stmts) {
|
|
|
1179
1265
|
continue;
|
|
1180
1266
|
}
|
|
1181
1267
|
if (Node.isVariableStatement(s)) {
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const
|
|
1186
|
-
const havocKey = havocMatch?.[2] ?? null; // //@ havoc key
|
|
1187
|
-
const isHavoc = !!havocMatch;
|
|
1268
|
+
const havoc = havocDirective(s);
|
|
1269
|
+
const havocType = havoc?.type ?? null; // //@ havoc : Type
|
|
1270
|
+
const havocKey = havoc?.key ?? null; // //@ havoc key
|
|
1271
|
+
const isHavoc = !!havoc;
|
|
1188
1272
|
for (const d of s.getDeclarations()) {
|
|
1189
1273
|
// Havoc on destructuring: emit each named binding as a separate havoced variable
|
|
1190
1274
|
const nameNode = d.getNameNode();
|
|
@@ -1625,7 +1709,11 @@ function extractStmts(stmts) {
|
|
|
1625
1709
|
// functions this would emit the wrong shape, but lsc has no current
|
|
1626
1710
|
// examples of explicit bare return in void functions; revisit if one
|
|
1627
1711
|
// appears.
|
|
1628
|
-
|
|
1712
|
+
// `//@ havoc <key>` on a return abstracts the matching calls or new
|
|
1713
|
+
// expressions inside the returned expression — there is no variable to
|
|
1714
|
+
// hang a whole-value havoc on, so only the key form applies here.
|
|
1715
|
+
const value = withHavocKey(havocDirective(s)?.key ?? null, () => expr ? extractExpr(expr) : { kind: "var", name: "undefined" });
|
|
1716
|
+
result.push({ kind: "return", value, line });
|
|
1629
1717
|
continue;
|
|
1630
1718
|
}
|
|
1631
1719
|
if (Node.isBreakStatement(s)) {
|
|
@@ -1641,14 +1729,12 @@ function extractStmts(stmts) {
|
|
|
1641
1729
|
// //@ havoc before `x = e` — discard the RHS, assign a nondeterministic
|
|
1642
1730
|
// value of x's type. Only applies to plain `=` with an identifier LHS;
|
|
1643
1731
|
// compound assigns, `arr[i] = v`, and `x++` fall through to desugaring.
|
|
1644
|
-
const
|
|
1645
|
-
|
|
1646
|
-
.find(m => m !== null);
|
|
1647
|
-
if (havocMatch && Node.isBinaryExpression(expr)
|
|
1732
|
+
const havoc = havocDirective(s);
|
|
1733
|
+
if (havoc && !havoc.key && Node.isBinaryExpression(expr)
|
|
1648
1734
|
&& expr.getOperatorToken().getText() === "="
|
|
1649
1735
|
&& Node.isIdentifier(expr.getLeft())) {
|
|
1650
1736
|
const target = expr.getLeft().getText();
|
|
1651
|
-
const tsType =
|
|
1737
|
+
const tsType = havoc.type ?? _eraseGenerics(typeToString(expr.getLeft().getType()));
|
|
1652
1738
|
result.push({ kind: "assign", target, value: { kind: "havoc", tsType }, line });
|
|
1653
1739
|
continue;
|
|
1654
1740
|
}
|
|
@@ -1838,6 +1924,12 @@ function extractFunctionInner(fn, parentAnnotations) {
|
|
|
1838
1924
|
return "void"; // Promise<void>
|
|
1839
1925
|
}
|
|
1840
1926
|
const node = fn.getReturnTypeNode();
|
|
1927
|
+
// A type predicate (`x is T` / `asserts x is T`) is a `boolean` at
|
|
1928
|
+
// runtime; the narrowing it performs is a TS-only refinement with no
|
|
1929
|
+
// counterpart in the model. Without this, `getText()` yields "x is T"
|
|
1930
|
+
// and the type mapper reads the subject name as an opaque type.
|
|
1931
|
+
if (node && node.getKind() === SyntaxKind.TypePredicate)
|
|
1932
|
+
return "boolean";
|
|
1841
1933
|
if (node && Node.isUnionTypeNode(node))
|
|
1842
1934
|
return _eraseGenerics(_tsTypeFromUnionNode(node));
|
|
1843
1935
|
if (node)
|
|
@@ -1870,6 +1962,7 @@ export function extractModule(sourceFile) {
|
|
|
1870
1962
|
// `extractExpr` during call extraction (only symbols *actually used* end up
|
|
1871
1963
|
// here), deduped by qualified name.
|
|
1872
1964
|
_externs.clear();
|
|
1965
|
+
_externSigTypes.length = 0;
|
|
1873
1966
|
// Share the module's ts-morph Project with parseTsType (scratch source file
|
|
1874
1967
|
// for type-string parsing). Done before declare-type parsing so any
|
|
1875
1968
|
// parseTsType call downstream uses the same Project.
|
|
@@ -2397,6 +2490,28 @@ export function extractModule(sourceFile) {
|
|
|
2397
2490
|
}
|
|
2398
2491
|
}
|
|
2399
2492
|
}
|
|
2493
|
+
// A constant's initializer can reference other constants
|
|
2494
|
+
// (`const ZERO_NINE = ZERO + nthDigit(-1)`). Close over those initializers
|
|
2495
|
+
// before filtering — mirroring the transitive type filter below — or a
|
|
2496
|
+
// constant reachable only from another constant is dropped and the backend
|
|
2497
|
+
// sees an undefined name.
|
|
2498
|
+
// Snapshot first: what the closure adds are VALUE references, and a value
|
|
2499
|
+
// and a type can share a name (`const Action = Schema.Literals(…)` next to
|
|
2500
|
+
// `type Action = Schema.Schema.Type<typeof Action>`). Keeping the constant
|
|
2501
|
+
// alive must not also drag in the unrelated type alias, so the type filter
|
|
2502
|
+
// below runs off the pre-closure set.
|
|
2503
|
+
const typeReferencedNames = new Set(referencedNames);
|
|
2504
|
+
for (let grew = true; grew;) {
|
|
2505
|
+
grew = false;
|
|
2506
|
+
for (const c of constants) {
|
|
2507
|
+
if (!referencedNames.has(c.name))
|
|
2508
|
+
continue;
|
|
2509
|
+
const before = referencedNames.size;
|
|
2510
|
+
collectNamesExpr(c.value);
|
|
2511
|
+
if (referencedNames.size !== before)
|
|
2512
|
+
grew = true;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2400
2515
|
constants.splice(0, constants.length, ...constants.filter(c => referencedNames.has(c.name)));
|
|
2401
2516
|
// Filter types to only those referenced by verified functions (transitive)
|
|
2402
2517
|
const neededTypes = new Set();
|
|
@@ -2415,7 +2530,7 @@ export function extractModule(sourceFile) {
|
|
|
2415
2530
|
for (const m of f.tsType.matchAll(/\b([A-Z]\w*)\b/g))
|
|
2416
2531
|
markType(m[1]);
|
|
2417
2532
|
}
|
|
2418
|
-
for (const name of
|
|
2533
|
+
for (const name of typeReferencedNames)
|
|
2419
2534
|
markType(name);
|
|
2420
2535
|
// Signature types also mark their base after stripping array/optional
|
|
2421
2536
|
// WRAPPERS (`Out[]`/`Msg | undefined` → `Out`/`Msg`), so a function returning
|
|
@@ -2431,13 +2546,12 @@ export function extractModule(sourceFile) {
|
|
|
2431
2546
|
// Resolve imported types: extract types referenced in function signatures but not in this file
|
|
2432
2547
|
const knownTypes = new Set(typeDecls.map(d => d.name));
|
|
2433
2548
|
const builtins = new Set(["Map", "Set", "Array", "String", "Number", "Boolean", "Promise", "Date", "RegExp", "Error"]);
|
|
2549
|
+
const visitedTypes = new Set();
|
|
2434
2550
|
function resolveType(t, locationNode) {
|
|
2435
|
-
//
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
}
|
|
2440
|
-
// Resolve type aliases (e.g. string unions imported from other files)
|
|
2551
|
+
// Resolve type aliases (e.g. string unions imported from other files).
|
|
2552
|
+
// BEFORE the visited guard: the same interned compilerType can arrive both
|
|
2553
|
+
// with and without its alias symbol (getTypeAtLocation drops it), and an
|
|
2554
|
+
// aliasless first visit must not suppress the alias extraction.
|
|
2441
2555
|
const alias = t.getAliasSymbol();
|
|
2442
2556
|
if (alias) {
|
|
2443
2557
|
const aliasName = alias.getName();
|
|
@@ -2470,6 +2584,18 @@ export function extractModule(sourceFile) {
|
|
|
2470
2584
|
}
|
|
2471
2585
|
}
|
|
2472
2586
|
}
|
|
2587
|
+
// Recursion guard: recursive unions (Expr → variant → body: Expr) are
|
|
2588
|
+
// reachable now that anonymous variant fields are walked below. Keyed on
|
|
2589
|
+
// the compiler's interned Type object — alias names are not enough,
|
|
2590
|
+
// because getTypeAtLocation can drop the alias symbol.
|
|
2591
|
+
if (visitedTypes.has(t.compilerType))
|
|
2592
|
+
return;
|
|
2593
|
+
visitedTypes.add(t.compilerType);
|
|
2594
|
+
// Unwrap arrays and generics to find user-defined types
|
|
2595
|
+
if (t.isArray()) {
|
|
2596
|
+
resolveType(t.getArrayElementTypeOrThrow(), locationNode);
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2473
2599
|
if (t.isUnion()) {
|
|
2474
2600
|
for (const u of t.getUnionTypes())
|
|
2475
2601
|
resolveType(u, locationNode);
|
|
@@ -2492,6 +2618,15 @@ export function extractModule(sourceFile) {
|
|
|
2492
2618
|
}
|
|
2493
2619
|
}
|
|
2494
2620
|
}
|
|
2621
|
+
else if (t.isObject() && (!name || name.startsWith("__")) && t.getCallSignatures().length === 0) {
|
|
2622
|
+
// Anonymous object type — typically a variant of an imported
|
|
2623
|
+
// discriminated union. There is no decl to extract, but its fields can
|
|
2624
|
+
// reference named types (`arms: MatchArm[]`) that downstream passes
|
|
2625
|
+
// need declared, so walk them.
|
|
2626
|
+
for (const prop of t.getProperties()) {
|
|
2627
|
+
resolveType(prop.getTypeAtLocation(locationNode), locationNode);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2495
2630
|
}
|
|
2496
2631
|
for (let i = 0; i < fnsToExtract.length; i++) {
|
|
2497
2632
|
const f = fnsToExtract[i];
|
|
@@ -2507,6 +2642,9 @@ export function extractModule(sourceFile) {
|
|
|
2507
2642
|
resolveType(p.getType(), p);
|
|
2508
2643
|
}
|
|
2509
2644
|
}
|
|
2645
|
+
// Extern signature types: see _externSigTypes.
|
|
2646
|
+
for (const r of _externSigTypes)
|
|
2647
|
+
resolveType(r.type, r.node);
|
|
2510
2648
|
// Resolve anonymous object return types into synthetic named types
|
|
2511
2649
|
for (let i = 0; i < fnsToExtract.length; i++) {
|
|
2512
2650
|
const f = fnsToExtract[i];
|
|
@@ -36,3 +36,71 @@ export function runInfo(raw, outPath) {
|
|
|
36
36
|
writeFileSync(outPath, JSON.stringify(out, null, 2) + "\n");
|
|
37
37
|
console.log(`Wrote ${outPath}`);
|
|
38
38
|
}
|
|
39
|
+
/** Statement/expression kinds present anywhere in a function body — a cheap
|
|
40
|
+
* structural census so consumers can classify (havoc/assume/throw usage)
|
|
41
|
+
* without receiving the whole body. */
|
|
42
|
+
function bodyKinds(fn) {
|
|
43
|
+
// Keys under which a Ty (not a statement/expression) hangs — their `kind`
|
|
44
|
+
// tags belong to the type grammar and would pollute the census.
|
|
45
|
+
const TY_KEYS = new Set(["ty", "binderTy", "varTy", "nameTypes"]);
|
|
46
|
+
const kinds = new Set();
|
|
47
|
+
const walk = (node) => {
|
|
48
|
+
if (Array.isArray(node)) {
|
|
49
|
+
node.forEach(walk);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (node && typeof node === "object") {
|
|
53
|
+
const o = node;
|
|
54
|
+
if (typeof o.kind === "string") {
|
|
55
|
+
kinds.add(o.kind === "assert" && o.assumed === true ? "assume" : o.kind);
|
|
56
|
+
}
|
|
57
|
+
for (const [k, v] of Object.entries(o))
|
|
58
|
+
if (!TY_KEYS.has(k))
|
|
59
|
+
walk(v);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
walk(fn.body);
|
|
63
|
+
return [...kinds].sort();
|
|
64
|
+
}
|
|
65
|
+
function typedFn(fn, rawFn) {
|
|
66
|
+
// Carry the original TS type string per parameter: Ty conflates types whose
|
|
67
|
+
// runtime shapes differ (Map<K,V> and Record<K,V> both map to kind "map"),
|
|
68
|
+
// and satellites that construct runtime values need to tell them apart.
|
|
69
|
+
const rawTsTypes = new Map((rawFn?.params ?? []).map(p => [p.name, p.tsType]));
|
|
70
|
+
return {
|
|
71
|
+
name: fn.name,
|
|
72
|
+
exported: rawFn?.exported ?? false,
|
|
73
|
+
typeParams: fn.typeParams,
|
|
74
|
+
params: fn.params.map(p => ({ ...p, tsType: rawTsTypes.get(p.name) })),
|
|
75
|
+
returnTy: fn.returnTy,
|
|
76
|
+
requires: fn.requires,
|
|
77
|
+
ensures: fn.ensures,
|
|
78
|
+
decreases: fn.decreases,
|
|
79
|
+
contract: rawFn?.contract ?? [],
|
|
80
|
+
isPure: fn.isPure,
|
|
81
|
+
forcePure: fn.forcePure,
|
|
82
|
+
autohavoc: fn.autohavoc,
|
|
83
|
+
bodyKinds: bodyKinds(fn),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function runTypedInfo(raw, typed, version, backendDirective, dafny) {
|
|
87
|
+
const rawByName = new Map(raw.functions.map(f => [f.name, f]));
|
|
88
|
+
const rawMethods = new Map(raw.classes.flatMap(c => c.methods.map(m => [`${c.name}.${m.name}`, m])));
|
|
89
|
+
const out = {
|
|
90
|
+
schema: 1,
|
|
91
|
+
lemmascript: version,
|
|
92
|
+
file: typed.file,
|
|
93
|
+
backendDirective,
|
|
94
|
+
typeDecls: typed.typeDecls,
|
|
95
|
+
externs: typed.externs,
|
|
96
|
+
constants: typed.constants,
|
|
97
|
+
functions: typed.functions.map(f => typedFn(f, rawByName.get(f.name))),
|
|
98
|
+
classes: typed.classes.map(c => ({
|
|
99
|
+
name: c.name,
|
|
100
|
+
fields: c.fields,
|
|
101
|
+
methods: c.methods.map(m => typedFn(m, rawMethods.get(`${c.name}.${m.name}`))),
|
|
102
|
+
})),
|
|
103
|
+
dafny,
|
|
104
|
+
};
|
|
105
|
+
console.log(JSON.stringify(out, null, 2));
|
|
106
|
+
}
|
package/tools/dist/ir.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* The emit phase pretty-prints them to backend syntax (Lean or Dafny).
|
|
6
6
|
*/
|
|
7
7
|
export const pWild = () => ({ kind: "wild" });
|
|
8
|
-
export const pCtor = (
|
|
8
|
+
export const pCtor = (c, ...binders) => ({ kind: "ctor", ctor: c, binders });
|
|
9
9
|
/** Binder identifiers a pattern introduces (`[]` for wildcard / nullary ctor). */
|
|
10
10
|
export function patternBinders(p) {
|
|
11
11
|
return p.kind === "ctor" ? p.binders : [];
|
|
@@ -17,19 +17,40 @@ export function patternCtor(p) {
|
|
|
17
17
|
export function patternBinds(p, name) {
|
|
18
18
|
return patternBinders(p).includes(name);
|
|
19
19
|
}
|
|
20
|
+
// ── Literal queries ──────────────────────────────────────────
|
|
21
|
+
/** The exact value of an integer-literal operand (possibly negated), or null
|
|
22
|
+
* when `e` isn't one. Answers in compiler-side `bigint` so a backend folding a
|
|
23
|
+
* literal into arithmetic stays exact: JS bitwise operators truncate to 32 bits
|
|
24
|
+
* and `Math.pow(2, n)` is a double, both of which lie past 2^53. A `num`
|
|
25
|
+
* outside the safe-integer range has already lost precision, so it is not a
|
|
26
|
+
* usable answer — hence null. */
|
|
27
|
+
export function exactIntegerLiteral(e) {
|
|
28
|
+
if (e.kind === "bigint")
|
|
29
|
+
return BigInt(e.value);
|
|
30
|
+
if (e.kind === "num")
|
|
31
|
+
return Number.isSafeInteger(e.value) ? BigInt(e.value) : null;
|
|
32
|
+
// Transform folds `-<num>` into a negative `num`, but leaves a negated
|
|
33
|
+
// `bigint` structural (folding it would coerce the payload through Number).
|
|
34
|
+
if (e.kind === "unop" && e.op === "-") {
|
|
35
|
+
const inner = exactIntegerLiteral(e.expr);
|
|
36
|
+
return inner === null ? null : -inner;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
20
40
|
export function anyExpr(e, pred) {
|
|
21
41
|
if (pred(e))
|
|
22
42
|
return true;
|
|
23
43
|
switch (e.kind) {
|
|
24
44
|
case "var":
|
|
25
45
|
case "num":
|
|
46
|
+
case "bigint":
|
|
26
47
|
case "bool":
|
|
27
48
|
case "str":
|
|
28
49
|
case "emptyMap":
|
|
29
50
|
case "emptySet":
|
|
30
51
|
case "havoc":
|
|
31
52
|
case "default": return false;
|
|
32
|
-
case "constructor": return
|
|
53
|
+
case "constructor": return e.args.some(a => anyExpr(a, pred));
|
|
33
54
|
case "binop": return anyExpr(e.left, pred) || anyExpr(e.right, pred);
|
|
34
55
|
case "unop":
|
|
35
56
|
case "toNat":
|
|
@@ -46,7 +67,7 @@ export function anyExpr(e, pred) {
|
|
|
46
67
|
case "methodCall": return anyExpr(e.obj, pred) || e.args.some(a => anyExpr(a, pred));
|
|
47
68
|
case "lambda": return e.body.some(s => anyExprInStmt(s, pred));
|
|
48
69
|
case "if": return anyExpr(e.cond, pred) || anyExpr(e.then, pred) || anyExpr(e.else, pred);
|
|
49
|
-
case "match": return
|
|
70
|
+
case "match": return anyExpr(e.scrutinee, pred) || e.arms.some(a => anyExpr(a.body, pred));
|
|
50
71
|
case "forall":
|
|
51
72
|
case "exists": return anyExpr(e.body, pred);
|
|
52
73
|
case "let": return anyExpr(e.value, pred) || anyExpr(e.body, pred);
|
|
@@ -65,7 +86,7 @@ export function anyExprInStmt(s, pred) {
|
|
|
65
86
|
case "break":
|
|
66
87
|
case "continue": return false;
|
|
67
88
|
case "if": return anyExpr(s.cond, pred) || anyExprInStmts(s.then, pred) || anyExprInStmts(s.else, pred);
|
|
68
|
-
case "match": return
|
|
89
|
+
case "match": return anyExpr(s.scrutinee, pred) || s.arms.some(a => anyExprInStmts(a.body, pred));
|
|
69
90
|
case "while": return anyExpr(s.cond, pred) || s.invariants.some(i => anyExpr(i, pred))
|
|
70
91
|
|| (s.decreasing ? anyExpr(s.decreasing, pred) : false) || (s.doneWith ? anyExpr(s.doneWith, pred) : false)
|
|
71
92
|
|| anyExprInStmts(s.body, pred);
|
|
@@ -80,10 +101,9 @@ export function anyExprInStmts(stmts, pred) {
|
|
|
80
101
|
// function. These drive the *local* freshness checks for user-facing binders
|
|
81
102
|
// (the result out-parameter, comprehension binders): a binder is checked only
|
|
82
103
|
// against the expressions/scope it actually wraps, not the whole module.
|
|
83
|
-
const _refsName = (name) => e => (e.kind === "var" && e.name === name) ||
|
|
104
|
+
const _refsName = (name) => (e) => (e.kind === "var" && e.name === name) ||
|
|
84
105
|
(e.kind === "app" && e.fn === name) ||
|
|
85
|
-
(e.kind === "constructor" && e.name === name)
|
|
86
|
-
(e.kind === "match" && typeof e.scrutinee === "string" && e.scrutinee === name);
|
|
106
|
+
(e.kind === "constructor" && e.name === name);
|
|
87
107
|
export function usesName(e, name) {
|
|
88
108
|
return anyExpr(e, _refsName(name));
|
|
89
109
|
}
|