lemmascript 0.3.3 → 0.5.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 +20 -13
- package/package.json +4 -1
- package/tools/dist/dafny-commands.js +31 -14
- package/tools/dist/dafny-emit.js +302 -17
- package/tools/dist/extract.js +1087 -181
- package/tools/dist/info-command.js +38 -0
- package/tools/dist/lean-emit.js +81 -5
- package/tools/dist/lsc.js +29 -9
- package/tools/dist/narrow.js +932 -0
- package/tools/dist/peephole.js +451 -0
- package/tools/dist/resolve.js +680 -258
- package/tools/dist/specparser.js +18 -2
- package/tools/dist/transform.js +597 -441
- package/tools/dist/types.js +128 -69
package/tools/dist/types.js
CHANGED
|
@@ -4,81 +4,140 @@
|
|
|
4
4
|
* Single source of truth for type-related decisions.
|
|
5
5
|
* The transform phase imports this.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
8
|
+
/**
|
|
9
|
+
* Parses TS type strings via a real ts-morph parse — no regex cascade. Set
|
|
10
|
+
* once per run by `extractModule` (so it reuses the module's Project); a
|
|
11
|
+
* separate in-memory Project is created lazily for callers that hit
|
|
12
|
+
* `parseTsType` outside the extract pipeline (e.g. tests, `lsc info` when
|
|
13
|
+
* driven independently).
|
|
14
|
+
*/
|
|
15
|
+
let _synthFile = null;
|
|
16
|
+
export function initTypeParser(project) {
|
|
17
|
+
_synthFile = project.createSourceFile("__lsc_type_parse__.ts", "", { overwrite: true });
|
|
18
|
+
}
|
|
19
|
+
function synthFile() {
|
|
20
|
+
if (_synthFile)
|
|
21
|
+
return _synthFile;
|
|
22
|
+
const p = new Project({ useInMemoryFileSystem: true });
|
|
23
|
+
_synthFile = p.createSourceFile("__lsc_type_parse__.ts", "");
|
|
24
|
+
return _synthFile;
|
|
23
25
|
}
|
|
24
26
|
export function parseTsType(tsType) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
const sf = synthFile();
|
|
28
|
+
sf.replaceWithText(`type __t = ${tsType};`);
|
|
29
|
+
const alias = sf.getTypeAliasOrThrow("__t");
|
|
30
|
+
return tyFromTypeNode(alias.getTypeNodeOrThrow());
|
|
31
|
+
}
|
|
32
|
+
function tyFromTypeNode(tn) {
|
|
33
|
+
if (Node.isParenthesizedTypeNode(tn))
|
|
34
|
+
return tyFromTypeNode(tn.getTypeNode());
|
|
35
|
+
if (Node.isUnionTypeNode(tn)) {
|
|
36
|
+
const arms = tn.getTypeNodes();
|
|
37
|
+
const isBoolLit = (a) => Node.isLiteralTypeNode(a) && (a.getLiteral().getKind() === SyntaxKind.TrueKeyword || a.getLiteral().getKind() === SyntaxKind.FalseKeyword);
|
|
38
|
+
const isNullish = (a) => a.getKind() === SyntaxKind.NullKeyword ||
|
|
39
|
+
a.getKind() === SyntaxKind.UndefinedKeyword ||
|
|
40
|
+
(Node.isLiteralTypeNode(a) && a.getLiteral().getKind() === SyntaxKind.NullKeyword);
|
|
41
|
+
// Collapse expanded boolean (`true | false`) into a single bool slot, so
|
|
42
|
+
// `boolean | undefined` (which TS expands to `false | true | undefined`)
|
|
43
|
+
// still reads as `optional<bool>` rather than falling through to user.
|
|
44
|
+
const hasTrueLit = arms.some(a => Node.isLiteralTypeNode(a) && a.getLiteral().getKind() === SyntaxKind.TrueKeyword);
|
|
45
|
+
const hasFalseLit = arms.some(a => Node.isLiteralTypeNode(a) && a.getLiteral().getKind() === SyntaxKind.FalseKeyword);
|
|
46
|
+
const collapseBool = hasTrueLit && hasFalseLit;
|
|
47
|
+
const normalized = collapseBool
|
|
48
|
+
? [{ syntheticBool: true }, ...arms.filter(a => !isBoolLit(a)).map(node => ({ node }))]
|
|
49
|
+
: arms.map(node => ({ node }));
|
|
50
|
+
if (normalized.length === 1 && "syntheticBool" in normalized[0])
|
|
51
|
+
return { kind: "bool" };
|
|
52
|
+
const nonNullish = normalized.filter(a => "syntheticBool" in a || !isNullish(a.node));
|
|
53
|
+
if (nonNullish.length === 1 && normalized.length >= 2) {
|
|
54
|
+
const sole = nonNullish[0];
|
|
55
|
+
const inner = "syntheticBool" in sole ? { kind: "bool" } : tyFromTypeNode(sole.node);
|
|
56
|
+
return { kind: "optional", inner };
|
|
35
57
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
// Multi-member union with a nullish arm (`A | B | undefined`): the
|
|
59
|
+
// structured part is opaque to us, but the optionality isn't. Expose it as
|
|
60
|
+
// optional<user "A | B"> so downstream (e.g. the let-type merge in resolve)
|
|
61
|
+
// can keep the nullability while taking structure from a more precise
|
|
62
|
+
// source. Without this the whole thing collapses to one opaque user blob.
|
|
63
|
+
const hadNullish = normalized.some(a => !("syntheticBool" in a) && isNullish(a.node));
|
|
64
|
+
if (hadNullish && nonNullish.length >= 2) {
|
|
65
|
+
const innerName = nonNullish.map(a => ("syntheticBool" in a ? "boolean" : a.node.getText())).join(" | ");
|
|
66
|
+
return { kind: "optional", inner: { kind: "user", name: innerName } };
|
|
39
67
|
}
|
|
68
|
+
// Other unions: leave as a user type spelled how the source wrote it.
|
|
69
|
+
return { kind: "user", name: tn.getText() };
|
|
40
70
|
}
|
|
41
|
-
if (
|
|
42
|
-
return { kind: "
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return { kind: "bool" };
|
|
49
|
-
if (t === "string")
|
|
50
|
-
return { kind: "string" };
|
|
51
|
-
if (t === "void" || t === "undefined")
|
|
52
|
-
return { kind: "void" };
|
|
53
|
-
if (t === "unknown")
|
|
54
|
-
return { kind: "unknown" };
|
|
55
|
-
// Record<K, V> → map
|
|
56
|
-
const recordMatch = t.match(/^Record<(.+)>$/);
|
|
57
|
-
if (recordMatch) {
|
|
58
|
-
const args = splitTypeArgs(recordMatch[1]);
|
|
59
|
-
if (args.length === 2)
|
|
60
|
-
return { kind: "map", key: parseTsType(args[0]), value: parseTsType(args[1]) };
|
|
71
|
+
if (Node.isArrayTypeNode(tn))
|
|
72
|
+
return { kind: "array", elem: tyFromTypeNode(tn.getElementTypeNode()) };
|
|
73
|
+
if (Node.isTupleTypeNode(tn)) {
|
|
74
|
+
const elems = tn.getElements();
|
|
75
|
+
if (elems.length === 0)
|
|
76
|
+
return { kind: "array", elem: { kind: "unknown" } };
|
|
77
|
+
return { kind: "array", elem: tyFromTypeNode(elems[0]) };
|
|
61
78
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const args = splitTypeArgs(mapMatch[1]);
|
|
70
|
-
if (args.length === 2)
|
|
71
|
-
return { kind: "map", key: parseTsType(args[0]), value: parseTsType(args[1]) };
|
|
79
|
+
if (Node.isFunctionTypeNode(tn)) {
|
|
80
|
+
const params = tn.getParameters().map(p => {
|
|
81
|
+
const ptn = p.getTypeNode();
|
|
82
|
+
return ptn ? tyFromTypeNode(ptn) : { kind: "unknown" };
|
|
83
|
+
});
|
|
84
|
+
const result = tyFromTypeNode(tn.getReturnTypeNodeOrThrow());
|
|
85
|
+
return { kind: "fn", params, result };
|
|
72
86
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
if (Node.isLiteralTypeNode(tn)) {
|
|
88
|
+
const lk = tn.getLiteral().getKind();
|
|
89
|
+
if (lk === SyntaxKind.TrueKeyword || lk === SyntaxKind.FalseKeyword)
|
|
90
|
+
return { kind: "bool" };
|
|
91
|
+
}
|
|
92
|
+
switch (tn.getKind()) {
|
|
93
|
+
case SyntaxKind.NumberKeyword:
|
|
94
|
+
case SyntaxKind.BigIntKeyword:
|
|
95
|
+
return { kind: "int" };
|
|
96
|
+
case SyntaxKind.BooleanKeyword:
|
|
97
|
+
return { kind: "bool" };
|
|
98
|
+
case SyntaxKind.StringKeyword:
|
|
99
|
+
return { kind: "string" };
|
|
100
|
+
case SyntaxKind.VoidKeyword:
|
|
101
|
+
case SyntaxKind.UndefinedKeyword:
|
|
102
|
+
return { kind: "void" };
|
|
103
|
+
case SyntaxKind.UnknownKeyword:
|
|
104
|
+
case SyntaxKind.AnyKeyword:
|
|
105
|
+
return { kind: "unknown" };
|
|
106
|
+
}
|
|
107
|
+
if (Node.isTypeReference(tn)) {
|
|
108
|
+
const name = tn.getTypeName().getText();
|
|
109
|
+
const args = tn.getTypeArguments();
|
|
110
|
+
if (name === "nat" && args.length === 0)
|
|
111
|
+
return { kind: "nat" };
|
|
112
|
+
if (name === "Array" && args.length === 1)
|
|
113
|
+
return { kind: "array", elem: tyFromTypeNode(args[0]) };
|
|
114
|
+
if (name === "Set" && args.length === 1)
|
|
115
|
+
return { kind: "set", elem: tyFromTypeNode(args[0]) };
|
|
116
|
+
if ((name === "Map" || name === "Record") && args.length === 2) {
|
|
117
|
+
return { kind: "map", key: tyFromTypeNode(args[0]), value: tyFromTypeNode(args[1]) };
|
|
118
|
+
}
|
|
119
|
+
// User-named type: include generic args in the spelled name so callers
|
|
120
|
+
// that key on the string (e.g. typedir alias lookups) see the exact form.
|
|
121
|
+
return { kind: "user", name: tn.getText() };
|
|
122
|
+
}
|
|
123
|
+
return { kind: "user", name: tn.getText() };
|
|
124
|
+
}
|
|
125
|
+
/** Render a Ty in LemmaScript canonical syntax — backend-neutral, side-effect-free.
|
|
126
|
+
* Used by `lsc info` for the signature field of `foo.ts.json`. */
|
|
127
|
+
export function tyToCanonical(ty) {
|
|
128
|
+
switch (ty.kind) {
|
|
129
|
+
case "bool": return "bool";
|
|
130
|
+
case "nat": return "nat";
|
|
131
|
+
case "int": return "int";
|
|
132
|
+
case "real": return "real";
|
|
133
|
+
case "string": return "string";
|
|
134
|
+
case "void": return "void";
|
|
135
|
+
case "unknown": return "unknown";
|
|
136
|
+
case "array": return `seq<${tyToCanonical(ty.elem)}>`;
|
|
137
|
+
case "map": return `map<${tyToCanonical(ty.key)}, ${tyToCanonical(ty.value)}>`;
|
|
138
|
+
case "set": return `set<${tyToCanonical(ty.elem)}>`;
|
|
139
|
+
case "optional": return `Option<${tyToCanonical(ty.inner)}>`;
|
|
140
|
+
case "user": return ty.name;
|
|
141
|
+
case "fn": return `(${ty.params.map(tyToCanonical).join(", ")}) -> ${tyToCanonical(ty.result)}`;
|
|
82
142
|
}
|
|
83
|
-
return { kind: "user", name: t };
|
|
84
143
|
}
|