@mirascript/typed 0.1.63 → 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.
@@ -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
+ }
@@ -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:PostfixType|1.., _ "|" _ | {
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
  ///////////////////////////////////////////////////////////////////////////////