@mirascript/typed 0.1.64 → 0.1.65
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 +2 -2
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +100 -43
- 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 +25 -0
- package/dist/simplifier.d.ts.map +1 -0
- package/dist/simplifier.js +274 -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 +109 -55
- package/src/parser.ts +49 -1
- package/src/simplifier.ts +327 -0
- package/src/stringify.ts +191 -0
- package/src/type.peggy +49 -2
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { GenericType, RecordField, RecordType, Type } from './parser.js';
|
|
2
|
+
|
|
3
|
+
/** Controls which type simplifications are applied. */
|
|
4
|
+
export interface SimplifyOptions {
|
|
5
|
+
/** Flatten nested union nodes. */
|
|
6
|
+
flattenUnions?: boolean;
|
|
7
|
+
/** Flatten nested intersection nodes. */
|
|
8
|
+
flattenIntersections?: boolean;
|
|
9
|
+
/** Remove duplicate members inside union nodes. */
|
|
10
|
+
deduplicateUnions?: boolean;
|
|
11
|
+
/** Remove duplicate members inside intersection nodes. */
|
|
12
|
+
deduplicateIntersections?: boolean;
|
|
13
|
+
/** Remove single-member union nodes. */
|
|
14
|
+
unwrapSingleUnion?: boolean;
|
|
15
|
+
/** Remove single-member intersection nodes. */
|
|
16
|
+
unwrapSingleIntersection?: boolean;
|
|
17
|
+
/** Distribute intersections over unions. */
|
|
18
|
+
distributeIntersectionsOverUnions?: boolean;
|
|
19
|
+
/** Merge intersections of explicit record fields. */
|
|
20
|
+
mergeRecordIntersections?: boolean;
|
|
21
|
+
/** Inline tuple spread elements (..[A, B] → A, B). */
|
|
22
|
+
expandTupleSpreads?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_OPTIONS: Required<SimplifyOptions> = {
|
|
26
|
+
flattenUnions: true,
|
|
27
|
+
flattenIntersections: true,
|
|
28
|
+
deduplicateUnions: true,
|
|
29
|
+
deduplicateIntersections: true,
|
|
30
|
+
unwrapSingleUnion: true,
|
|
31
|
+
unwrapSingleIntersection: true,
|
|
32
|
+
distributeIntersectionsOverUnions: true,
|
|
33
|
+
mergeRecordIntersections: true,
|
|
34
|
+
expandTupleSpreads: true,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Fills in default simplification options. */
|
|
38
|
+
function normalizeOptions(options?: SimplifyOptions): Required<SimplifyOptions> {
|
|
39
|
+
return { ...DEFAULT_OPTIONS, ...options };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Checks whether a type is represented by an object node. */
|
|
43
|
+
function isTypeObject(type: Type): type is Exclude<Type, GenericType | string> {
|
|
44
|
+
return typeof type === 'object';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Checks whether a record type uses the explicit fields form. */
|
|
48
|
+
function isFieldRecordType(type: Type): type is Extract<RecordType, { fields: RecordField[] }> {
|
|
49
|
+
return isTypeObject(type) && type.kind === 'record' && 'fields' in type;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Builds a stable key for type-level deduplication within one simplify call. */
|
|
53
|
+
function getTypeDedupKey(type: Type, symbols: Map<symbol, number>): string {
|
|
54
|
+
if (typeof type === 'string') return `string:${type}`;
|
|
55
|
+
if (typeof type === 'symbol') {
|
|
56
|
+
const existing = symbols.get(type);
|
|
57
|
+
if (existing != null) return `symbol:${existing}`;
|
|
58
|
+
const next = symbols.size + 1;
|
|
59
|
+
symbols.set(type, next);
|
|
60
|
+
return `symbol:${next}`;
|
|
61
|
+
}
|
|
62
|
+
switch (type.kind) {
|
|
63
|
+
case 'array':
|
|
64
|
+
return `array:${getTypeDedupKey(type.element, symbols)}`;
|
|
65
|
+
case 'union':
|
|
66
|
+
return `union:[${type.types.map((t) => getTypeDedupKey(t, symbols)).join(',')}]`;
|
|
67
|
+
case 'intersection':
|
|
68
|
+
return `intersection:[${type.types.map((t) => getTypeDedupKey(t, symbols)).join(',')}]`;
|
|
69
|
+
case 'record':
|
|
70
|
+
if ('fields' in type) {
|
|
71
|
+
return `recordFields:[${type.fields
|
|
72
|
+
.map((f) => `${f.name}:${String(Boolean(f.optional))}:${getTypeDedupKey(f.type, symbols)}`)
|
|
73
|
+
.join(',')}]`;
|
|
74
|
+
}
|
|
75
|
+
return `recordKV:${type.key == null ? 'none' : getTypeDedupKey(type.key, symbols)}:${getTypeDedupKey(type.value, symbols)}`;
|
|
76
|
+
case 'literal':
|
|
77
|
+
return `literal:${typeof type.value}:${String(type.value)}`;
|
|
78
|
+
case 'template':
|
|
79
|
+
return `template:[${type.parts.map((p) => getTypeDedupKey(p, symbols)).join(',')}]`;
|
|
80
|
+
case 'function':
|
|
81
|
+
return `function:${
|
|
82
|
+
type.name ?? ''
|
|
83
|
+
}:<${(type.typeParams ?? []).map((p) => getTypeDedupKey(p, symbols)).join(',')}>(${type.params
|
|
84
|
+
.map((p) => `${p.name}:${String(Boolean(p.spread))}:${getTypeDedupKey(p.type, symbols)}`)
|
|
85
|
+
.join(',')})=>${type.returns == null ? 'void' : getTypeDedupKey(type.returns, symbols)}`;
|
|
86
|
+
case 'tuple':
|
|
87
|
+
return `tuple:[${type.elements
|
|
88
|
+
.map((e) => `${String(Boolean(e.spread))}:${getTypeDedupKey(e.type, symbols)}`)
|
|
89
|
+
.join(',')}]`;
|
|
90
|
+
case 'reflection':
|
|
91
|
+
return `reflection:${type.name}`;
|
|
92
|
+
default:
|
|
93
|
+
return 'unknown';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Removes duplicate members from union/intersection type member lists. */
|
|
98
|
+
function deduplicateTypeMembers(types: Type[]): Type[] {
|
|
99
|
+
const symbols = new Map<symbol, number>();
|
|
100
|
+
const seen = new Set<string>();
|
|
101
|
+
const result: Type[] = [];
|
|
102
|
+
for (const type of types) {
|
|
103
|
+
const key = getTypeDedupKey(type, symbols);
|
|
104
|
+
if (seen.has(key)) continue;
|
|
105
|
+
seen.add(key);
|
|
106
|
+
result.push(type);
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Flattens nested union nodes when the corresponding option is enabled. */
|
|
112
|
+
function flattenUnionTypes(types: Type[], options: Required<SimplifyOptions>): Type[] {
|
|
113
|
+
if (!options.flattenUnions) return types;
|
|
114
|
+
const result: Type[] = [];
|
|
115
|
+
for (const type of types) {
|
|
116
|
+
if (isTypeObject(type) && type.kind === 'union') {
|
|
117
|
+
result.push(...flattenUnionTypes(type.types, options));
|
|
118
|
+
} else {
|
|
119
|
+
result.push(type);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Flattens nested intersection nodes when the corresponding option is enabled. */
|
|
126
|
+
function flattenIntersectionTypes(types: Type[], options: Required<SimplifyOptions>): Type[] {
|
|
127
|
+
if (!options.flattenIntersections) return types;
|
|
128
|
+
const result: Type[] = [];
|
|
129
|
+
for (const type of types) {
|
|
130
|
+
if (isTypeObject(type) && type.kind === 'intersection') {
|
|
131
|
+
result.push(...flattenIntersectionTypes(type.types, options));
|
|
132
|
+
} else {
|
|
133
|
+
result.push(type);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Simplifies a record field recursively. */
|
|
140
|
+
function simplifyRecordField(field: RecordField, options: Required<SimplifyOptions>): RecordField {
|
|
141
|
+
return {
|
|
142
|
+
...field,
|
|
143
|
+
type: simplifyImpl(field.type, options),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Merges explicit record fields across an intersection. */
|
|
148
|
+
function mergeRecordFieldIntersections(types: Array<Extract<RecordType, { fields: RecordField[] }>>): Type {
|
|
149
|
+
const merged = new Map<string, { optional: boolean; type: Type }>();
|
|
150
|
+
for (const record of types) {
|
|
151
|
+
for (const field of record.fields) {
|
|
152
|
+
const prev = merged.get(field.name);
|
|
153
|
+
if (prev == null) {
|
|
154
|
+
merged.set(field.name, {
|
|
155
|
+
optional: field.optional ?? false,
|
|
156
|
+
type: field.type,
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
merged.set(field.name, {
|
|
161
|
+
optional: (prev.optional ?? false) && (field.optional ?? false),
|
|
162
|
+
type: {
|
|
163
|
+
kind: 'intersection',
|
|
164
|
+
types: [prev.type, field.type],
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
kind: 'record',
|
|
172
|
+
fields: Array.from(merged.entries()).map(([name, field]) => ({
|
|
173
|
+
name,
|
|
174
|
+
optional: field.optional,
|
|
175
|
+
type: field.type,
|
|
176
|
+
})),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Distributes intersections over unions using a cartesian product. */
|
|
181
|
+
function distributeIntersectionsOverUnions(types: Type[], options: Required<SimplifyOptions>): Type {
|
|
182
|
+
let combinations: Type[][] = [[]];
|
|
183
|
+
for (const type of types) {
|
|
184
|
+
const choices = isTypeObject(type) && type.kind === 'union' ? type.types : [type];
|
|
185
|
+
const next: Type[][] = [];
|
|
186
|
+
for (const combo of combinations) {
|
|
187
|
+
for (const choice of choices) {
|
|
188
|
+
next.push([...combo, choice]);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
combinations = next;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const branches = combinations.map((combo) =>
|
|
195
|
+
simplifyImpl(
|
|
196
|
+
{ kind: 'intersection', types: combo },
|
|
197
|
+
{
|
|
198
|
+
...options,
|
|
199
|
+
distributeIntersectionsOverUnions: false,
|
|
200
|
+
},
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
if (branches.length === 1) return branches[0]!;
|
|
204
|
+
return { kind: 'union', types: branches };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Simplifies a Type AST in place, optionally disabling individual normalization passes. */
|
|
208
|
+
function simplifyImpl(type: Type, config: Required<SimplifyOptions>): Type {
|
|
209
|
+
if (typeof type === 'symbol' || typeof type === 'string') {
|
|
210
|
+
return type;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (type.kind === 'array') {
|
|
214
|
+
type.element = simplifyImpl(type.element, config);
|
|
215
|
+
return type;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (type.kind === 'function') {
|
|
219
|
+
for (const param of type.params) {
|
|
220
|
+
param.type = simplifyImpl(param.type, config);
|
|
221
|
+
}
|
|
222
|
+
if (type.returns != null) type.returns = simplifyImpl(type.returns, config);
|
|
223
|
+
return type;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (type.kind === 'literal') {
|
|
227
|
+
return type;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (type.kind === 'template') {
|
|
231
|
+
type.parts = type.parts.map((part) => simplifyImpl(part, config));
|
|
232
|
+
return type;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (type.kind === 'tuple') {
|
|
236
|
+
type.elements = type.elements.flatMap((element) => {
|
|
237
|
+
const simplifiedType = simplifyImpl(element.type, config);
|
|
238
|
+
if (
|
|
239
|
+
config.expandTupleSpreads &&
|
|
240
|
+
element.spread &&
|
|
241
|
+
typeof simplifiedType === 'object' &&
|
|
242
|
+
simplifiedType.kind === 'tuple'
|
|
243
|
+
) {
|
|
244
|
+
// Inline tuple spread: ..[A, B] → A, B
|
|
245
|
+
return simplifiedType.elements;
|
|
246
|
+
}
|
|
247
|
+
return [{ ...element, type: simplifiedType }];
|
|
248
|
+
});
|
|
249
|
+
return type;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (type.kind === 'reflection') {
|
|
253
|
+
return type;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (type.kind === 'record') {
|
|
257
|
+
if ('fields' in type) {
|
|
258
|
+
type.fields = type.fields.map((field) => simplifyRecordField(field, config));
|
|
259
|
+
} else {
|
|
260
|
+
if (type.key != null) type.key = simplifyImpl(type.key, config);
|
|
261
|
+
type.value = simplifyImpl(type.value, config);
|
|
262
|
+
}
|
|
263
|
+
return type;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (type.kind === 'union') {
|
|
267
|
+
let simplifiedTypes = flattenUnionTypes(
|
|
268
|
+
type.types.map((item) => simplifyImpl(item, config)),
|
|
269
|
+
config,
|
|
270
|
+
);
|
|
271
|
+
if (config.deduplicateUnions) {
|
|
272
|
+
simplifiedTypes = deduplicateTypeMembers(simplifiedTypes);
|
|
273
|
+
}
|
|
274
|
+
if (config.unwrapSingleUnion && simplifiedTypes.length === 1) {
|
|
275
|
+
return simplifiedTypes[0]!;
|
|
276
|
+
}
|
|
277
|
+
type.types = simplifiedTypes;
|
|
278
|
+
return type;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (type.kind === 'intersection') {
|
|
282
|
+
let simplifiedTypes = flattenIntersectionTypes(
|
|
283
|
+
type.types.map((item) => simplifyImpl(item, config)),
|
|
284
|
+
config,
|
|
285
|
+
);
|
|
286
|
+
if (config.deduplicateIntersections) {
|
|
287
|
+
simplifiedTypes = deduplicateTypeMembers(simplifiedTypes);
|
|
288
|
+
}
|
|
289
|
+
if (
|
|
290
|
+
config.distributeIntersectionsOverUnions &&
|
|
291
|
+
simplifiedTypes.some((item) => isTypeObject(item) && item.kind === 'union')
|
|
292
|
+
) {
|
|
293
|
+
return distributeIntersectionsOverUnions(simplifiedTypes, config);
|
|
294
|
+
}
|
|
295
|
+
if (config.mergeRecordIntersections) {
|
|
296
|
+
const recordTypes = simplifiedTypes.filter(isFieldRecordType);
|
|
297
|
+
if (recordTypes.length >= 2) {
|
|
298
|
+
const nonRecordTypes = simplifiedTypes.filter((item) => !isFieldRecordType(item));
|
|
299
|
+
const mergedRecord = simplifyImpl(mergeRecordFieldIntersections(recordTypes), config);
|
|
300
|
+
let mergedTypes = [mergedRecord, ...nonRecordTypes];
|
|
301
|
+
if (config.deduplicateIntersections) {
|
|
302
|
+
mergedTypes = deduplicateTypeMembers(mergedTypes);
|
|
303
|
+
}
|
|
304
|
+
if (config.unwrapSingleIntersection && mergedTypes.length === 1) {
|
|
305
|
+
return mergedTypes[0]!;
|
|
306
|
+
}
|
|
307
|
+
type.types = mergedTypes;
|
|
308
|
+
return type;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (config.unwrapSingleIntersection && simplifiedTypes.length === 1) {
|
|
312
|
+
return simplifiedTypes[0]!;
|
|
313
|
+
}
|
|
314
|
+
type.types = simplifiedTypes;
|
|
315
|
+
return type;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* c8 ignore next 3 */
|
|
319
|
+
(type) satisfies never;
|
|
320
|
+
return type;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Simplifies a Type AST in place, optionally disabling individual normalization passes. */
|
|
324
|
+
export function simplify(type: Type, options?: SimplifyOptions): Type {
|
|
325
|
+
const config = normalizeOptions(options);
|
|
326
|
+
return simplifyImpl(type, config);
|
|
327
|
+
}
|
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
|
+
}
|
package/src/type.peggy
CHANGED
|
@@ -37,6 +37,11 @@ function union(types) {
|
|
|
37
37
|
return { kind: "union", types };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function intersection(types) {
|
|
41
|
+
if (types.length === 1) return types[0];
|
|
42
|
+
return { kind: "intersection", types };
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
function record(fields) {
|
|
41
46
|
for(let i = 0; i < fields.length; i++) {
|
|
42
47
|
fields[i].name ??= String(i);
|
|
@@ -55,6 +60,10 @@ function string(parts) {
|
|
|
55
60
|
}
|
|
56
61
|
return { kind: 'template', parts };
|
|
57
62
|
}
|
|
63
|
+
|
|
64
|
+
function tupleElement(type, spread) {
|
|
65
|
+
return { type, spread: spread ?? false };
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
Start
|
|
@@ -62,17 +71,22 @@ Start
|
|
|
62
71
|
/ _ @Type _
|
|
63
72
|
|
|
64
73
|
///////////////////////////////////////////////////////////////////////////////
|
|
65
|
-
// Type (union)
|
|
74
|
+
// Type (union / intersection)
|
|
66
75
|
///////////////////////////////////////////////////////////////////////////////
|
|
67
76
|
|
|
68
77
|
Type
|
|
69
78
|
= UnionType
|
|
70
79
|
|
|
71
80
|
UnionType
|
|
72
|
-
= _ ( "|" _ )? u:
|
|
81
|
+
= _ ( "|" _ )? u:IntersectionType|1.., _ "|" _ | {
|
|
73
82
|
return union(u);
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
IntersectionType
|
|
86
|
+
= _ ( "&" _ )? i:PostfixType|1.., _ "&" _ | {
|
|
87
|
+
return intersection(i);
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
///////////////////////////////////////////////////////////////////////////////
|
|
77
91
|
// Array
|
|
78
92
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -91,6 +105,8 @@ PostfixType
|
|
|
91
105
|
|
|
92
106
|
PrimaryType
|
|
93
107
|
= FunctionType
|
|
108
|
+
/ ReflectionType
|
|
109
|
+
/ TupleType
|
|
94
110
|
/ name:Identifier generic:(_ "<" _ @TypeArgList _ ">")? {
|
|
95
111
|
if (generic) {
|
|
96
112
|
if (name === 'array') {
|
|
@@ -110,6 +126,10 @@ PrimaryType
|
|
|
110
126
|
if (BUILT_IN_TYPES.has(name)) {
|
|
111
127
|
return name;
|
|
112
128
|
}
|
|
129
|
+
// `type` is a non-reserved keyword — allow it as a plain named type
|
|
130
|
+
if (name === 'type') {
|
|
131
|
+
return name;
|
|
132
|
+
}
|
|
113
133
|
return named(name);
|
|
114
134
|
}
|
|
115
135
|
/ lit:String {
|
|
@@ -120,6 +140,33 @@ PrimaryType
|
|
|
120
140
|
/ RecordType
|
|
121
141
|
|
|
122
142
|
|
|
143
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
144
|
+
// Reflection
|
|
145
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
146
|
+
|
|
147
|
+
ReflectionType
|
|
148
|
+
= "type" _ "(" _ name:Identifier _ ")" {
|
|
149
|
+
return { kind: "reflection", name };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
153
|
+
// Tuple
|
|
154
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
155
|
+
|
|
156
|
+
TupleType
|
|
157
|
+
= "[" _ elements:TupleElementList? _ "]" {
|
|
158
|
+
return { kind: "tuple", elements: elements ?? [] };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
TupleElementList
|
|
162
|
+
= @TupleElement|1.., _ "," _ | ( _ "," _ )?
|
|
163
|
+
|
|
164
|
+
TupleElement
|
|
165
|
+
= spread:".."? _ type:Type {
|
|
166
|
+
return tupleElement(type, spread != null);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
123
170
|
///////////////////////////////////////////////////////////////////////////////
|
|
124
171
|
// Generics
|
|
125
172
|
///////////////////////////////////////////////////////////////////////////////
|