@mirascript/typed 0.1.64 → 0.1.70
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 +38 -27
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +5 -2
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +129 -48
- package/dist/json.js.map +1 -1
- package/dist/parser.d.ts +29 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +13 -0
- package/dist/parser.js.map +1 -1
- package/dist/simplifier.d.ts +48 -0
- package/dist/simplifier.d.ts.map +1 -0
- package/dist/simplifier.js +332 -0
- package/dist/simplifier.js.map +1 -0
- package/dist/stringify.d.ts +7 -0
- package/dist/stringify.d.ts.map +1 -0
- package/dist/stringify.js +186 -0
- package/dist/stringify.js.map +1 -0
- package/dist/type.js +601 -289
- package/dist/type.js.map +1 -1
- package/package.json +7 -6
- package/src/index.ts +2 -0
- package/src/json.ts +142 -59
- package/src/parser.ts +49 -1
- package/src/simplifier.ts +404 -0
- package/src/stringify.ts +191 -0
- package/src/type.peggy +49 -2
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import type { GenericType, RecordField, RecordType, Type } from './parser.js';
|
|
2
|
+
|
|
3
|
+
/** Top types that can be absorbed / eliminated in simplification. */
|
|
4
|
+
type TopType = 'unknown' | 'never' | 'any';
|
|
5
|
+
|
|
6
|
+
/** Controls which type simplifications are applied. */
|
|
7
|
+
export interface SimplifyOptions {
|
|
8
|
+
/** Flatten nested union nodes. */
|
|
9
|
+
flattenUnions?: boolean;
|
|
10
|
+
/** Flatten nested intersection nodes. */
|
|
11
|
+
flattenIntersections?: boolean;
|
|
12
|
+
/** Remove duplicate members inside union nodes. */
|
|
13
|
+
deduplicateUnions?: boolean;
|
|
14
|
+
/** Remove duplicate members inside intersection nodes. */
|
|
15
|
+
deduplicateIntersections?: boolean;
|
|
16
|
+
/** Remove single-member union nodes. */
|
|
17
|
+
unwrapSingleUnion?: boolean;
|
|
18
|
+
/** Remove single-member intersection nodes. */
|
|
19
|
+
unwrapSingleIntersection?: boolean;
|
|
20
|
+
/** Distribute intersections over unions. */
|
|
21
|
+
distributeIntersectionsOverUnions?: boolean;
|
|
22
|
+
/** Merge intersections of explicit record fields. */
|
|
23
|
+
mergeRecordIntersections?: boolean;
|
|
24
|
+
/** Inline tuple spread elements (..[A, B] → A, B). */
|
|
25
|
+
expandTupleSpreads?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Eliminate / absorb top types in unions.
|
|
28
|
+
* - `unknown | T` → `unknown`
|
|
29
|
+
* - `any | T` → `any`
|
|
30
|
+
* - `T | never` → `T`
|
|
31
|
+
* `true` enables all, or pass a subset of `'unknown' | 'never' | 'any'`.
|
|
32
|
+
*/
|
|
33
|
+
simplifyTopTypesInUnions?: boolean | TopType[];
|
|
34
|
+
/**
|
|
35
|
+
* Eliminate / absorb top types in intersections.
|
|
36
|
+
* - `never & T` → `never`
|
|
37
|
+
* - `T & unknown` → `T`
|
|
38
|
+
* - `T & any` → `T`
|
|
39
|
+
* `true` enables all, or pass a subset of `'unknown' | 'never' | 'any'`.
|
|
40
|
+
*/
|
|
41
|
+
simplifyTopTypesInIntersections?: boolean | TopType[];
|
|
42
|
+
/** `record<string, V>` → `record<V>` (string is the default key). */
|
|
43
|
+
normalizeGenericRecord?: boolean;
|
|
44
|
+
/** `array<any | unknown>` → `array` (no element constraint). */
|
|
45
|
+
normalizeGenericArray?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Resolves the top types option into an array of top types. */
|
|
49
|
+
function resolveTopTypes(value: boolean | TopType[] | undefined): TopType[] {
|
|
50
|
+
if (value === false || value == null) return [];
|
|
51
|
+
if (value === true) return ['unknown', 'never', 'any'];
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULT_OPTIONS: Required<SimplifyOptions> = {
|
|
56
|
+
flattenUnions: true,
|
|
57
|
+
flattenIntersections: true,
|
|
58
|
+
deduplicateUnions: true,
|
|
59
|
+
deduplicateIntersections: true,
|
|
60
|
+
unwrapSingleUnion: true,
|
|
61
|
+
unwrapSingleIntersection: true,
|
|
62
|
+
distributeIntersectionsOverUnions: true,
|
|
63
|
+
mergeRecordIntersections: true,
|
|
64
|
+
expandTupleSpreads: true,
|
|
65
|
+
simplifyTopTypesInUnions: true,
|
|
66
|
+
simplifyTopTypesInIntersections: true,
|
|
67
|
+
normalizeGenericRecord: true,
|
|
68
|
+
normalizeGenericArray: true,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Fills in default simplification options. */
|
|
72
|
+
function normalizeOptions(options?: SimplifyOptions): Required<SimplifyOptions> {
|
|
73
|
+
return { ...DEFAULT_OPTIONS, ...options };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Checks whether a type is represented by an object node. */
|
|
77
|
+
function isTypeObject(type: Type): type is Exclude<Type, GenericType | string> {
|
|
78
|
+
return typeof type === 'object';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Checks whether a record type uses the explicit fields form. */
|
|
82
|
+
function isFieldRecordType(type: Type): type is Extract<RecordType, { fields: RecordField[] }> {
|
|
83
|
+
return isTypeObject(type) && type.kind === 'record' && 'fields' in type;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Builds a stable key for type-level deduplication within one simplify call. */
|
|
87
|
+
function getTypeDedupKey(type: Type, symbols: Map<symbol, number>): string {
|
|
88
|
+
if (typeof type === 'string') return `string:${type}`;
|
|
89
|
+
if (typeof type === 'symbol') {
|
|
90
|
+
const existing = symbols.get(type);
|
|
91
|
+
if (existing != null) return `symbol:${existing}`;
|
|
92
|
+
const next = symbols.size + 1;
|
|
93
|
+
symbols.set(type, next);
|
|
94
|
+
return `symbol:${next}`;
|
|
95
|
+
}
|
|
96
|
+
switch (type.kind) {
|
|
97
|
+
case 'array':
|
|
98
|
+
return `array:${getTypeDedupKey(type.element, symbols)}`;
|
|
99
|
+
case 'union':
|
|
100
|
+
return `union:[${type.types.map((t) => getTypeDedupKey(t, symbols)).join(',')}]`;
|
|
101
|
+
case 'intersection':
|
|
102
|
+
return `intersection:[${type.types.map((t) => getTypeDedupKey(t, symbols)).join(',')}]`;
|
|
103
|
+
case 'record':
|
|
104
|
+
if ('fields' in type) {
|
|
105
|
+
return `recordFields:[${type.fields
|
|
106
|
+
.map((f) => `${f.name}:${String(Boolean(f.optional))}:${getTypeDedupKey(f.type, symbols)}`)
|
|
107
|
+
.join(',')}]`;
|
|
108
|
+
}
|
|
109
|
+
return `recordKV:${type.key == null ? 'none' : getTypeDedupKey(type.key, symbols)}:${getTypeDedupKey(type.value, symbols)}`;
|
|
110
|
+
case 'literal':
|
|
111
|
+
return `literal:${typeof type.value}:${String(type.value)}`;
|
|
112
|
+
case 'template':
|
|
113
|
+
return `template:[${type.parts.map((p) => getTypeDedupKey(p, symbols)).join(',')}]`;
|
|
114
|
+
case 'function':
|
|
115
|
+
return `function:${
|
|
116
|
+
type.name ?? ''
|
|
117
|
+
}:<${(type.typeParams ?? []).map((p) => getTypeDedupKey(p, symbols)).join(',')}>(${type.params
|
|
118
|
+
.map((p) => `${p.name}:${String(Boolean(p.spread))}:${getTypeDedupKey(p.type, symbols)}`)
|
|
119
|
+
.join(',')})=>${type.returns == null ? 'void' : getTypeDedupKey(type.returns, symbols)}`;
|
|
120
|
+
case 'tuple':
|
|
121
|
+
return `tuple:[${type.elements
|
|
122
|
+
.map((e) => `${String(Boolean(e.spread))}:${getTypeDedupKey(e.type, symbols)}`)
|
|
123
|
+
.join(',')}]`;
|
|
124
|
+
case 'reflection':
|
|
125
|
+
return `reflection:${type.name}`;
|
|
126
|
+
default:
|
|
127
|
+
return 'unknown';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Removes duplicate members from union/intersection type member lists. */
|
|
132
|
+
function deduplicateTypeMembers(types: Type[]): Type[] {
|
|
133
|
+
if (types.length <= 1) return types;
|
|
134
|
+
const symbols = new Map<symbol, number>();
|
|
135
|
+
const seen = new Set<string>();
|
|
136
|
+
const result: Type[] = [];
|
|
137
|
+
for (const type of types) {
|
|
138
|
+
const key = getTypeDedupKey(type, symbols);
|
|
139
|
+
if (seen.has(key)) continue;
|
|
140
|
+
seen.add(key);
|
|
141
|
+
result.push(type);
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Flattens nested union nodes when the corresponding option is enabled. */
|
|
147
|
+
function flattenUnionTypes(types: Type[], options: Required<SimplifyOptions>): Type[] {
|
|
148
|
+
if (!options.flattenUnions) return types;
|
|
149
|
+
const result: Type[] = [];
|
|
150
|
+
for (const type of types) {
|
|
151
|
+
if (isTypeObject(type) && type.kind === 'union') {
|
|
152
|
+
result.push(...flattenUnionTypes(type.types, options));
|
|
153
|
+
} else {
|
|
154
|
+
result.push(type);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Flattens nested intersection nodes when the corresponding option is enabled. */
|
|
161
|
+
function flattenIntersectionTypes(types: Type[], options: Required<SimplifyOptions>): Type[] {
|
|
162
|
+
if (!options.flattenIntersections) return types;
|
|
163
|
+
const result: Type[] = [];
|
|
164
|
+
for (const type of types) {
|
|
165
|
+
if (isTypeObject(type) && type.kind === 'intersection') {
|
|
166
|
+
result.push(...flattenIntersectionTypes(type.types, options));
|
|
167
|
+
} else {
|
|
168
|
+
result.push(type);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Simplifies a record field recursively. */
|
|
175
|
+
function simplifyRecordField(field: RecordField, options: Required<SimplifyOptions>): RecordField {
|
|
176
|
+
return {
|
|
177
|
+
...field,
|
|
178
|
+
type: simplifyImpl(field.type, options),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Merges explicit record fields across an intersection. */
|
|
183
|
+
function mergeRecordFieldIntersections(types: Array<Extract<RecordType, { fields: RecordField[] }>>): Type {
|
|
184
|
+
const merged = new Map<string, { optional: boolean; type: Type }>();
|
|
185
|
+
for (const record of types) {
|
|
186
|
+
for (const field of record.fields) {
|
|
187
|
+
const prev = merged.get(field.name);
|
|
188
|
+
if (prev == null) {
|
|
189
|
+
merged.set(field.name, {
|
|
190
|
+
optional: field.optional ?? false,
|
|
191
|
+
type: field.type,
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
merged.set(field.name, {
|
|
196
|
+
optional: (prev.optional ?? false) && (field.optional ?? false),
|
|
197
|
+
type: {
|
|
198
|
+
kind: 'intersection',
|
|
199
|
+
types: [prev.type, field.type],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
kind: 'record',
|
|
207
|
+
fields: Array.from(merged.entries()).map(([name, field]) => ({
|
|
208
|
+
name,
|
|
209
|
+
optional: field.optional,
|
|
210
|
+
type: field.type,
|
|
211
|
+
})),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Distributes intersections over unions using a cartesian product. */
|
|
216
|
+
function distributeIntersectionsOverUnions(types: Type[], options: Required<SimplifyOptions>): Type {
|
|
217
|
+
let combinations: Type[][] = [[]];
|
|
218
|
+
for (const type of types) {
|
|
219
|
+
const choices = isTypeObject(type) && type.kind === 'union' ? type.types : [type];
|
|
220
|
+
const next: Type[][] = [];
|
|
221
|
+
for (const combo of combinations) {
|
|
222
|
+
for (const choice of choices) {
|
|
223
|
+
next.push([...combo, choice]);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
combinations = next;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const branches = combinations.map((combo) =>
|
|
230
|
+
simplifyImpl(
|
|
231
|
+
{ kind: 'intersection', types: combo },
|
|
232
|
+
{
|
|
233
|
+
...options,
|
|
234
|
+
distributeIntersectionsOverUnions: false,
|
|
235
|
+
},
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
if (branches.length === 1) return branches[0]!;
|
|
239
|
+
return { kind: 'union', types: branches };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Simplifies a Type AST in place, optionally disabling individual normalization passes. */
|
|
243
|
+
function simplifyImpl(type: Type, config: Required<SimplifyOptions>): Type {
|
|
244
|
+
if (typeof type === 'symbol' || typeof type === 'string') {
|
|
245
|
+
return type;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (type.kind === 'array') {
|
|
249
|
+
type.element = simplifyImpl(type.element, config);
|
|
250
|
+
if (config.normalizeGenericArray && (type.element === 'any' || type.element === 'unknown')) {
|
|
251
|
+
return 'array';
|
|
252
|
+
}
|
|
253
|
+
return type;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (type.kind === 'function') {
|
|
257
|
+
for (const param of type.params) {
|
|
258
|
+
param.type = simplifyImpl(param.type, config);
|
|
259
|
+
}
|
|
260
|
+
if (type.returns != null) type.returns = simplifyImpl(type.returns, config);
|
|
261
|
+
return type;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (type.kind === 'literal') {
|
|
265
|
+
return type;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (type.kind === 'template') {
|
|
269
|
+
type.parts = type.parts.map((part) => simplifyImpl(part, config));
|
|
270
|
+
return type;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (type.kind === 'tuple') {
|
|
274
|
+
type.elements = type.elements.flatMap((element) => {
|
|
275
|
+
const simplifiedType = simplifyImpl(element.type, config);
|
|
276
|
+
if (
|
|
277
|
+
config.expandTupleSpreads &&
|
|
278
|
+
element.spread &&
|
|
279
|
+
typeof simplifiedType === 'object' &&
|
|
280
|
+
simplifiedType.kind === 'tuple'
|
|
281
|
+
) {
|
|
282
|
+
// Inline tuple spread: ..[A, B] → A, B
|
|
283
|
+
return simplifiedType.elements;
|
|
284
|
+
}
|
|
285
|
+
return [{ ...element, type: simplifiedType }];
|
|
286
|
+
});
|
|
287
|
+
return type;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (type.kind === 'reflection') {
|
|
291
|
+
return type;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (type.kind === 'record') {
|
|
295
|
+
if ('fields' in type) {
|
|
296
|
+
type.fields = type.fields.map((field) => simplifyRecordField(field, config));
|
|
297
|
+
} else {
|
|
298
|
+
if (type.key != null) type.key = simplifyImpl(type.key, config);
|
|
299
|
+
type.value = simplifyImpl(type.value, config);
|
|
300
|
+
if (config.normalizeGenericRecord && type.key === 'string') {
|
|
301
|
+
delete type.key;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return type;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (type.kind === 'union') {
|
|
308
|
+
let simplifiedTypes = flattenUnionTypes(
|
|
309
|
+
type.types.map((item) => simplifyImpl(item, config)),
|
|
310
|
+
config,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Top-type elimination: unknown | T → unknown, any | T → any, T | never → T
|
|
314
|
+
const topTypes = resolveTopTypes(config.simplifyTopTypesInUnions);
|
|
315
|
+
if (topTypes.length > 0) {
|
|
316
|
+
if (topTypes.includes('any') && simplifiedTypes.includes('any')) return 'any';
|
|
317
|
+
if (topTypes.includes('unknown') && simplifiedTypes.includes('unknown')) return 'unknown';
|
|
318
|
+
if (topTypes.includes('never')) {
|
|
319
|
+
simplifiedTypes = simplifiedTypes.filter((t) => t !== 'never');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (config.deduplicateUnions) {
|
|
324
|
+
simplifiedTypes = deduplicateTypeMembers(simplifiedTypes);
|
|
325
|
+
}
|
|
326
|
+
if (config.unwrapSingleUnion) {
|
|
327
|
+
if (simplifiedTypes.length === 1) return simplifiedTypes[0]!;
|
|
328
|
+
if (simplifiedTypes.length === 0) return 'never';
|
|
329
|
+
}
|
|
330
|
+
type.types = simplifiedTypes;
|
|
331
|
+
return type;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (type.kind === 'intersection') {
|
|
335
|
+
let simplifiedTypes = flattenIntersectionTypes(
|
|
336
|
+
type.types.map((item) => simplifyImpl(item, config)),
|
|
337
|
+
config,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Top-type elimination: never & T → never, T & unknown → T, T & any → T
|
|
341
|
+
const topTypes = resolveTopTypes(config.simplifyTopTypesInIntersections);
|
|
342
|
+
if (topTypes.length > 0) {
|
|
343
|
+
const typeNames = new Set(simplifiedTypes.filter((t): t is string => typeof t === 'string'));
|
|
344
|
+
if (topTypes.includes('never') && typeNames.has('never')) return 'never';
|
|
345
|
+
let filtered = false;
|
|
346
|
+
if (topTypes.includes('unknown') && typeNames.has('unknown')) {
|
|
347
|
+
simplifiedTypes = simplifiedTypes.filter((t) => t !== 'unknown');
|
|
348
|
+
filtered = true;
|
|
349
|
+
}
|
|
350
|
+
if (topTypes.includes('any') && typeNames.has('any')) {
|
|
351
|
+
simplifiedTypes = simplifiedTypes.filter((t) => t !== 'any');
|
|
352
|
+
filtered = true;
|
|
353
|
+
}
|
|
354
|
+
if (filtered) {
|
|
355
|
+
if (simplifiedTypes.length === 1) return simplifiedTypes[0]!;
|
|
356
|
+
if (simplifiedTypes.length === 0) {
|
|
357
|
+
// All members were the same eliminated top type
|
|
358
|
+
return typeNames.has('any') ? 'any' : 'unknown';
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (config.deduplicateIntersections) {
|
|
364
|
+
simplifiedTypes = deduplicateTypeMembers(simplifiedTypes);
|
|
365
|
+
}
|
|
366
|
+
if (
|
|
367
|
+
config.distributeIntersectionsOverUnions &&
|
|
368
|
+
simplifiedTypes.some((item) => isTypeObject(item) && item.kind === 'union')
|
|
369
|
+
) {
|
|
370
|
+
return distributeIntersectionsOverUnions(simplifiedTypes, config);
|
|
371
|
+
}
|
|
372
|
+
if (config.mergeRecordIntersections) {
|
|
373
|
+
const recordTypes = simplifiedTypes.filter(isFieldRecordType);
|
|
374
|
+
if (recordTypes.length >= 2) {
|
|
375
|
+
const nonRecordTypes = simplifiedTypes.filter((item) => !isFieldRecordType(item));
|
|
376
|
+
const mergedRecord = simplifyImpl(mergeRecordFieldIntersections(recordTypes), config);
|
|
377
|
+
let mergedTypes = [mergedRecord, ...nonRecordTypes];
|
|
378
|
+
if (config.deduplicateIntersections) {
|
|
379
|
+
mergedTypes = deduplicateTypeMembers(mergedTypes);
|
|
380
|
+
}
|
|
381
|
+
if (config.unwrapSingleIntersection && mergedTypes.length === 1) {
|
|
382
|
+
return mergedTypes[0]!;
|
|
383
|
+
}
|
|
384
|
+
type.types = mergedTypes;
|
|
385
|
+
return type;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (config.unwrapSingleIntersection && simplifiedTypes.length === 1) {
|
|
389
|
+
return simplifiedTypes[0]!;
|
|
390
|
+
}
|
|
391
|
+
type.types = simplifiedTypes;
|
|
392
|
+
return type;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* c8 ignore next 3 */
|
|
396
|
+
(type) satisfies never;
|
|
397
|
+
return type;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Simplifies a Type AST in place, optionally disabling individual normalization passes. */
|
|
401
|
+
export function simplify(type: Type, options?: SimplifyOptions): Type {
|
|
402
|
+
const config = normalizeOptions(options);
|
|
403
|
+
return simplifyImpl(type, config);
|
|
404
|
+
}
|
package/src/stringify.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { REG_IDENTIFIER_FULL } from '@mirascript/constants';
|
|
2
|
+
import type { Type } from './parser.js';
|
|
3
|
+
|
|
4
|
+
/** Precedence levels for parenthesization. */
|
|
5
|
+
const PREC_UNION = 1;
|
|
6
|
+
const PREC_INTERSECTION = 2;
|
|
7
|
+
|
|
8
|
+
/** Escape a string literal value for use in double-quoted strings. */
|
|
9
|
+
function escapeString(value: string): string {
|
|
10
|
+
return value
|
|
11
|
+
.replaceAll('\\', '\\\\')
|
|
12
|
+
.replaceAll('"', String.raw`\"`)
|
|
13
|
+
.replaceAll('\n', String.raw`\n`)
|
|
14
|
+
.replaceAll('\r', String.raw`\r`)
|
|
15
|
+
.replaceAll('\t', String.raw`\t`)
|
|
16
|
+
.replaceAll('$', String.raw`\$`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns the precedence level of a type node.
|
|
21
|
+
* Higher number = tighter binding.
|
|
22
|
+
*/
|
|
23
|
+
function getPrecedence(type: Type): number {
|
|
24
|
+
if (typeof type === 'string' || typeof type === 'symbol') return 0;
|
|
25
|
+
if (type.kind === 'union') return PREC_UNION;
|
|
26
|
+
if (type.kind === 'intersection') return PREC_INTERSECTION;
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Stringify a type, adding parentheses if the child's precedence
|
|
32
|
+
* is lower than the parent's required precedence.
|
|
33
|
+
*/
|
|
34
|
+
function stringifyImpl(type: Type, parentPrecedence: number): string {
|
|
35
|
+
// ---- primitives ----
|
|
36
|
+
if (typeof type === 'symbol') {
|
|
37
|
+
return type.description ?? '';
|
|
38
|
+
}
|
|
39
|
+
if (typeof type === 'string') {
|
|
40
|
+
return type;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- compound types ----
|
|
44
|
+
let result: string;
|
|
45
|
+
|
|
46
|
+
switch (type.kind) {
|
|
47
|
+
case 'literal': {
|
|
48
|
+
if (typeof type.value === 'boolean') {
|
|
49
|
+
return String(type.value);
|
|
50
|
+
} else {
|
|
51
|
+
return `"${escapeString(type.value)}"`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'template': {
|
|
56
|
+
let tpl = '`';
|
|
57
|
+
for (const part of type.parts) {
|
|
58
|
+
if (typeof part === 'object' && part.kind === 'literal' && typeof part.value === 'string') {
|
|
59
|
+
tpl += part.value.replaceAll('`', '\\`').replaceAll('$', String.raw`\$`);
|
|
60
|
+
} else {
|
|
61
|
+
tpl += `$(${stringifyImpl(part, 0)})`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
tpl += '`';
|
|
65
|
+
return tpl;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case 'array': {
|
|
69
|
+
const inner = stringifyImpl(type.element, 0);
|
|
70
|
+
// Use postfix notation for simple types, generic for complex
|
|
71
|
+
if (typeof type.element === 'string' || typeof type.element === 'symbol') {
|
|
72
|
+
result = `${inner}[]`;
|
|
73
|
+
} else if (type.element.kind === 'literal' || type.element.kind === 'reflection') {
|
|
74
|
+
result = `${inner}[]`;
|
|
75
|
+
} else {
|
|
76
|
+
return `array<${inner}>`;
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'union': {
|
|
82
|
+
const parts = type.types.map((t) => stringifyImpl(t, PREC_UNION));
|
|
83
|
+
result = parts.join(' | ');
|
|
84
|
+
if (parentPrecedence > PREC_UNION) return `(${result})`;
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case 'intersection': {
|
|
89
|
+
const parts = type.types.map((t) => stringifyImpl(t, PREC_INTERSECTION));
|
|
90
|
+
result = parts.join(' & ');
|
|
91
|
+
if (parentPrecedence > PREC_INTERSECTION) return `(${result})`;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'record': {
|
|
96
|
+
if ('fields' in type) {
|
|
97
|
+
if (type.fields.length === 0) {
|
|
98
|
+
return '()';
|
|
99
|
+
}
|
|
100
|
+
// Check if all fields are anonymous (name === positional index)
|
|
101
|
+
const allAnonymous = type.fields.every((f, i) => f.name === String(i) && !f.optional);
|
|
102
|
+
if (allAnonymous) {
|
|
103
|
+
const types = type.fields.map((f) => stringifyImpl(f.type, 0));
|
|
104
|
+
// Single anonymous field needs trailing comma to distinguish from grouping
|
|
105
|
+
const trailing = type.fields.length === 1 ? ',' : '';
|
|
106
|
+
return `(${types.join(', ')}${trailing})`;
|
|
107
|
+
} else {
|
|
108
|
+
const fields = type.fields.map((f) => {
|
|
109
|
+
const name = REG_IDENTIFIER_FULL.test(f.name) ? f.name : `"${escapeString(f.name)}"`;
|
|
110
|
+
const colon = f.optional ? '?:' : ':';
|
|
111
|
+
const typeStr = stringifyImpl(f.type, 0);
|
|
112
|
+
return `${name}${colon} ${typeStr}`;
|
|
113
|
+
});
|
|
114
|
+
return `(${fields.join(', ')})`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Generic record
|
|
119
|
+
if (type.key == null) {
|
|
120
|
+
return `record<${stringifyImpl(type.value, 0)}>`;
|
|
121
|
+
} else {
|
|
122
|
+
return `record<${stringifyImpl(type.key, 0)}, ${stringifyImpl(type.value, 0)}>`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case 'function': {
|
|
127
|
+
let fn = 'fn';
|
|
128
|
+
if (type.name != null) fn += ` ${type.name}`;
|
|
129
|
+
if (type.typeParams != null && type.typeParams.length > 0) {
|
|
130
|
+
fn += `<${type.typeParams.map((t) => stringifyImpl(t, 0)).join(', ')}>`;
|
|
131
|
+
}
|
|
132
|
+
const params = type.params.map((p) => {
|
|
133
|
+
const spread = p.spread ? '..' : '';
|
|
134
|
+
const name = p.name || (p.spread ? '' : '_');
|
|
135
|
+
const typeStr = p.type === 'any' && !p.spread ? '' : `: ${stringifyImpl(p.type, 0)}`;
|
|
136
|
+
return `${spread}${name}${typeStr}`;
|
|
137
|
+
});
|
|
138
|
+
fn += `(${params.join(', ')})`;
|
|
139
|
+
if (type.returns != null) {
|
|
140
|
+
const ret = stringifyImpl(type.returns, 0);
|
|
141
|
+
const needsParens =
|
|
142
|
+
typeof type.returns === 'object' &&
|
|
143
|
+
(type.returns.kind === 'union' || type.returns.kind === 'intersection');
|
|
144
|
+
fn += ` -> ${needsParens ? `(${ret})` : ret}`;
|
|
145
|
+
}
|
|
146
|
+
result = fn;
|
|
147
|
+
// Function types need parens inside union / intersection
|
|
148
|
+
if (parentPrecedence > 0) return `(${result})`;
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case 'tuple': {
|
|
153
|
+
if (type.elements.length === 0) {
|
|
154
|
+
result = '[]';
|
|
155
|
+
} else {
|
|
156
|
+
const elements = type.elements.map((e) => {
|
|
157
|
+
const spread = e.spread ? '..' : '';
|
|
158
|
+
return `${spread}${stringifyImpl(e.type, 0)}`;
|
|
159
|
+
});
|
|
160
|
+
result = `[${elements.join(', ')}]`;
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case 'reflection': {
|
|
166
|
+
result = `type(${type.name})`;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* c8 ignore next 3 */
|
|
171
|
+
default:
|
|
172
|
+
(type) satisfies never;
|
|
173
|
+
return 'unknown';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Wrap in parens if needed (for non-union/intersection types that
|
|
177
|
+
// appear inside higher-precedence context, e.g. function in union)
|
|
178
|
+
const childPrec = getPrecedence(type);
|
|
179
|
+
if (parentPrecedence > childPrec) {
|
|
180
|
+
return `(${result})`;
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Converts a Type AST back into a MiraScript type string.
|
|
187
|
+
* The output is guaranteed to be parseable by {@link parse}.
|
|
188
|
+
*/
|
|
189
|
+
export function stringify(type: Type): string {
|
|
190
|
+
return stringifyImpl(type, 0);
|
|
191
|
+
}
|