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.
@@ -4,81 +4,140 @@
4
4
  * Single source of truth for type-related decisions.
5
5
  * The transform phase imports this.
6
6
  */
7
- /** Split generic type arguments respecting nested angle brackets. */
8
- function splitTypeArgs(s) {
9
- const args = [];
10
- let depth = 0, start = 0;
11
- for (let i = 0; i < s.length; i++) {
12
- if (s[i] === '<')
13
- depth++;
14
- else if (s[i] === '>')
15
- depth--;
16
- else if (s[i] === ',' && depth === 0) {
17
- args.push(s.slice(start, i));
18
- start = i + 1;
19
- }
20
- }
21
- args.push(s.slice(start));
22
- return args.map(a => a.trim());
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 t = tsType.trim();
26
- // Union: T | undefined → optional<T>
27
- if (t.includes(" | ")) {
28
- let arms = t.split(" | ").map(a => a.trim());
29
- // Normalize expanded boolean literals: true | false → boolean
30
- const boolLits = new Set(["true", "false"]);
31
- const hasBoth = arms.includes("true") && arms.includes("false");
32
- if (hasBoth) {
33
- arms = arms.filter(a => !boolLits.has(a));
34
- arms.unshift("boolean");
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
- const nonUndef = arms.filter(a => a !== "undefined");
37
- if (nonUndef.length === 1 && arms.length === 2) {
38
- return { kind: "optional", inner: parseTsType(nonUndef[0]) };
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 (t === "number")
42
- return { kind: "int" };
43
- if (t === "bigint")
44
- return { kind: "int" };
45
- if (t === "nat")
46
- return { kind: "nat" };
47
- if (t === "boolean" || t === "true" || t === "false")
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
- // Array<T> or T[]
63
- const m = t.match(/^(?:Array<(.+)>|(.+)\[\])$/);
64
- if (m)
65
- return { kind: "array", elem: parseTsType(m[1] || m[2]) };
66
- // Map<K, V>
67
- const mapMatch = t.match(/^Map<(.+)>$/);
68
- if (mapMatch) {
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
- // Set<T>
74
- const setMatch = t.match(/^Set<(.+)>$/);
75
- if (setMatch)
76
- return { kind: "set", elem: parseTsType(setMatch[1]) };
77
- // Tuple [T1, T2, ...] → array of the common element type
78
- const tupleMatch = t.match(/^\[(.+)\]$/);
79
- if (tupleMatch) {
80
- const elems = splitTypeArgs(tupleMatch[1]);
81
- return { kind: "array", elem: parseTsType(elems[0]) };
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
  }