rip-lang 3.1.1 → 3.2.1
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 +23 -1
- package/bin/rip +24 -2
- package/docs/RIP-INTERNALS.md +18 -8
- package/docs/RIP-LANG.md +41 -6
- package/docs/RIP-REACTIVITY.md +21 -0
- package/docs/RIP-TYPES.md +1345 -595
- package/docs/dist/rip.browser.js +943 -343
- package/docs/dist/rip.browser.min.js +233 -231
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/package.json +3 -3
- package/src/compiler.js +36 -6
- package/src/grammar/grammar.rip +10 -0
- package/src/grammar/solar.rip +55 -136
- package/src/lexer.js +14 -2
- package/src/parser.js +254 -252
- package/src/types.js +718 -0
package/src/types.js
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
// Type System — Optional type annotations and .d.ts emission for Rip
|
|
2
|
+
//
|
|
3
|
+
// Architecture: installTypeSupport(Lexer) adds rewriteTypes() to the lexer
|
|
4
|
+
// prototype (sidecar pattern, same as components.js for CodeGenerator).
|
|
5
|
+
// emitTypes(tokens) generates .d.ts from annotated tokens before parsing.
|
|
6
|
+
// generateEnum() is the one CodeGenerator method for runtime enum output.
|
|
7
|
+
//
|
|
8
|
+
// Design: Types are fully resolved at the token level. The parser never sees
|
|
9
|
+
// type annotations, type aliases, or interfaces — only enum crosses into the
|
|
10
|
+
// grammar because it emits runtime JavaScript.
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// installTypeSupport — adds rewriteTypes() to Lexer.prototype
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export function installTypeSupport(Lexer) {
|
|
17
|
+
let proto = Lexer.prototype;
|
|
18
|
+
|
|
19
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
20
|
+
// rewriteTypes() — strip type annotations, collect type declarations
|
|
21
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
22
|
+
//
|
|
23
|
+
// Scans the token stream for:
|
|
24
|
+
// :: (TYPE_ANNOTATION) — collects type string, stores on surviving token
|
|
25
|
+
// ::= (TYPE_ALIAS) — collects type body, replaces with TYPE_DECL marker
|
|
26
|
+
// INTERFACE — collects body, replaces with TYPE_DECL marker
|
|
27
|
+
// DEF IDENTIFIER<...> — collects generic params via .spaced detection
|
|
28
|
+
//
|
|
29
|
+
// After this pass, the token stream is type-free (except ENUM tokens and
|
|
30
|
+
// TYPE_DECL markers that emitTypes() reads before they're filtered out).
|
|
31
|
+
|
|
32
|
+
proto.rewriteTypes = function() {
|
|
33
|
+
let tokens = this.tokens;
|
|
34
|
+
let gen = (tag, val, origin) => {
|
|
35
|
+
let t = [tag, val];
|
|
36
|
+
t.pre = 0;
|
|
37
|
+
t.data = null;
|
|
38
|
+
t.loc = origin?.loc ?? {r: 0, c: 0, n: 0};
|
|
39
|
+
t.spaced = false;
|
|
40
|
+
t.newLine = false;
|
|
41
|
+
t.generated = true;
|
|
42
|
+
if (origin) t.origin = origin;
|
|
43
|
+
return t;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.scanTokens((token, i, tokens) => {
|
|
47
|
+
let tag = token[0];
|
|
48
|
+
|
|
49
|
+
// ── Generic type parameters: DEF name<T>(...) or Name<T> ::= ───────
|
|
50
|
+
if (tag === 'IDENTIFIER') {
|
|
51
|
+
let next = tokens[i + 1];
|
|
52
|
+
if (next && next[0] === 'COMPARE' && next[1] === '<' && !next.spaced) {
|
|
53
|
+
let isDef = tokens[i - 1]?.[0] === 'DEF';
|
|
54
|
+
let genTokens = collectBalancedAngles(tokens, i + 1);
|
|
55
|
+
if (genTokens) {
|
|
56
|
+
let isAlias = !isDef && tokens[i + 1 + genTokens.length]?.[0] === 'TYPE_ALIAS';
|
|
57
|
+
if (isDef || isAlias) {
|
|
58
|
+
if (!token.data) token.data = {};
|
|
59
|
+
token.data.typeParams = genTokens.map(t => t[1]).join('');
|
|
60
|
+
tokens.splice(i + 1, genTokens.length);
|
|
61
|
+
// After removing <T>, retag ( as CALL_START if it follows DEF IDENTIFIER
|
|
62
|
+
if (isDef && tokens[i + 1]?.[0] === '(') {
|
|
63
|
+
tokens[i + 1][0] = 'CALL_START';
|
|
64
|
+
// Find matching ) and retag as CALL_END
|
|
65
|
+
let d = 1, m = i + 2;
|
|
66
|
+
while (m < tokens.length && d > 0) {
|
|
67
|
+
if (tokens[m][0] === '(' || tokens[m][0] === 'CALL_START') d++;
|
|
68
|
+
if (tokens[m][0] === ')' || tokens[m][0] === 'CALL_END') d--;
|
|
69
|
+
if (d === 0) tokens[m][0] = 'CALL_END';
|
|
70
|
+
m++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── TYPE_ANNOTATION (::) — collect type, store on token ─────────────
|
|
79
|
+
if (tag === 'TYPE_ANNOTATION') {
|
|
80
|
+
let prevToken = tokens[i - 1];
|
|
81
|
+
if (!prevToken) return 1;
|
|
82
|
+
|
|
83
|
+
let typeTokens = collectTypeExpression(tokens, i + 1);
|
|
84
|
+
let typeStr = buildTypeString(typeTokens);
|
|
85
|
+
|
|
86
|
+
// Find the token that survives into the s-expression
|
|
87
|
+
let target = prevToken;
|
|
88
|
+
let propName = 'type';
|
|
89
|
+
|
|
90
|
+
if (prevToken[0] === 'CALL_END' || prevToken[0] === ')') {
|
|
91
|
+
// Return type on DEF with parameters — scan backward to function name
|
|
92
|
+
let d = 1, k = i - 2;
|
|
93
|
+
while (k >= 0 && d > 0) {
|
|
94
|
+
let kTag = tokens[k][0];
|
|
95
|
+
if (kTag === 'CALL_END' || kTag === ')') d++;
|
|
96
|
+
if (kTag === 'CALL_START' || kTag === '(') d--;
|
|
97
|
+
k--;
|
|
98
|
+
}
|
|
99
|
+
if (k >= 0) target = tokens[k];
|
|
100
|
+
propName = 'returnType';
|
|
101
|
+
} else if (prevToken[0] === 'PARAM_END') {
|
|
102
|
+
// Return type on arrow function — scan forward to -> token
|
|
103
|
+
let arrowIdx = i + 1 + typeTokens.length;
|
|
104
|
+
let arrowToken = tokens[arrowIdx];
|
|
105
|
+
if (arrowToken && (arrowToken[0] === '->' || arrowToken[0] === '=>')) {
|
|
106
|
+
target = arrowToken;
|
|
107
|
+
}
|
|
108
|
+
propName = 'returnType';
|
|
109
|
+
} else if (prevToken[0] === 'IDENTIFIER' && i >= 2 &&
|
|
110
|
+
tokens[i - 2]?.[0] === 'DEF') {
|
|
111
|
+
// Return type on parameterless function: def foo:: string
|
|
112
|
+
propName = 'returnType';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!target.data) target.data = {};
|
|
116
|
+
target.data[propName] = typeStr;
|
|
117
|
+
|
|
118
|
+
// Remove :: and type tokens from stream
|
|
119
|
+
let removeCount = 1 + typeTokens.length;
|
|
120
|
+
tokens.splice(i, removeCount);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── TYPE_ALIAS (::=) — collect type body, create TYPE_DECL marker ───
|
|
125
|
+
if (tag === 'TYPE_ALIAS') {
|
|
126
|
+
let nameToken = tokens[i - 1];
|
|
127
|
+
if (!nameToken) return 1;
|
|
128
|
+
let name = nameToken[1];
|
|
129
|
+
let exported = i >= 2 && tokens[i - 2]?.[0] === 'EXPORT';
|
|
130
|
+
let removeFrom = exported ? i - 2 : i - 1;
|
|
131
|
+
let next = tokens[i + 1];
|
|
132
|
+
|
|
133
|
+
let makeDecl = (typeText) => {
|
|
134
|
+
let dt = gen('TYPE_DECL', name, nameToken);
|
|
135
|
+
dt.data = { name, typeText, exported };
|
|
136
|
+
if (nameToken.data?.typeParams) dt.data.typeParams = nameToken.data.typeParams;
|
|
137
|
+
return dt;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Structural type: Name ::= type INDENT ... OUTDENT
|
|
141
|
+
if (next && next[0] === 'IDENTIFIER' && next[1] === 'type' &&
|
|
142
|
+
tokens[i + 2]?.[0] === 'INDENT') {
|
|
143
|
+
let endIdx = findMatchingOutdent(tokens, i + 2);
|
|
144
|
+
tokens.splice(removeFrom, endIdx - removeFrom + 1, makeDecl(collectStructuralType(tokens, i + 2)));
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Block union: Name ::= TERMINATOR INDENT | "a" | "b" ... OUTDENT
|
|
149
|
+
if (next && (next[0] === 'TERMINATOR' || next[0] === 'INDENT')) {
|
|
150
|
+
let result = collectBlockUnion(tokens, i + 1);
|
|
151
|
+
if (result) {
|
|
152
|
+
tokens.splice(removeFrom, result.endIdx - removeFrom + 1, makeDecl(result.typeText));
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Simple alias: Name ::= type-expression
|
|
158
|
+
let typeTokens = collectTypeExpression(tokens, i + 1);
|
|
159
|
+
tokens.splice(removeFrom, i + 1 + typeTokens.length - removeFrom, makeDecl(buildTypeString(typeTokens)));
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── INTERFACE — collect body, create TYPE_DECL marker ───────────────
|
|
164
|
+
if (tag === 'INTERFACE') {
|
|
165
|
+
let exported = i >= 1 && tokens[i - 1]?.[0] === 'EXPORT';
|
|
166
|
+
let nameIdx = i + 1;
|
|
167
|
+
let nameToken = tokens[nameIdx];
|
|
168
|
+
if (!nameToken) return 1;
|
|
169
|
+
let name = nameToken[1];
|
|
170
|
+
|
|
171
|
+
let extendsName = null;
|
|
172
|
+
let bodyIdx = nameIdx + 1;
|
|
173
|
+
|
|
174
|
+
// Check for extends
|
|
175
|
+
if (tokens[bodyIdx]?.[0] === 'EXTENDS') {
|
|
176
|
+
extendsName = tokens[bodyIdx + 1]?.[1];
|
|
177
|
+
bodyIdx = bodyIdx + 2;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Collect body
|
|
181
|
+
if (tokens[bodyIdx]?.[0] === 'INDENT') {
|
|
182
|
+
let typeText = collectStructuralType(tokens, bodyIdx);
|
|
183
|
+
let endIdx = findMatchingOutdent(tokens, bodyIdx);
|
|
184
|
+
let declToken = gen('TYPE_DECL', name, nameToken);
|
|
185
|
+
declToken.data = {
|
|
186
|
+
name,
|
|
187
|
+
kind: 'interface',
|
|
188
|
+
extends: extendsName,
|
|
189
|
+
typeText,
|
|
190
|
+
exported
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
let removeFrom = exported ? i - 1 : i;
|
|
194
|
+
let removeCount = endIdx - removeFrom + 1;
|
|
195
|
+
tokens.splice(removeFrom, removeCount, declToken);
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return 1;
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Type expression collection helpers
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
// Collect type expression tokens starting at position j, respecting brackets
|
|
212
|
+
function collectTypeExpression(tokens, j) {
|
|
213
|
+
let typeTokens = [];
|
|
214
|
+
let depth = 0;
|
|
215
|
+
|
|
216
|
+
while (j < tokens.length) {
|
|
217
|
+
let t = tokens[j];
|
|
218
|
+
let tTag = t[0];
|
|
219
|
+
|
|
220
|
+
// Bracket balancing
|
|
221
|
+
let isOpen = tTag === '(' || tTag === '[' ||
|
|
222
|
+
tTag === 'CALL_START' || tTag === 'PARAM_START' || tTag === 'INDEX_START' ||
|
|
223
|
+
(tTag === 'COMPARE' && t[1] === '<');
|
|
224
|
+
let isClose = tTag === ')' || tTag === ']' ||
|
|
225
|
+
tTag === 'CALL_END' || tTag === 'PARAM_END' || tTag === 'INDEX_END' ||
|
|
226
|
+
(tTag === 'COMPARE' && t[1] === '>');
|
|
227
|
+
|
|
228
|
+
// Handle >> as two > closes (nested generics: Map<string, Set<number>>)
|
|
229
|
+
if (tTag === 'SHIFT' && t[1] === '>>' && depth >= 2) {
|
|
230
|
+
depth -= 2;
|
|
231
|
+
typeTokens.push(t);
|
|
232
|
+
j++;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (isOpen) {
|
|
237
|
+
depth++;
|
|
238
|
+
typeTokens.push(t);
|
|
239
|
+
j++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (isClose) {
|
|
243
|
+
if (depth > 0) {
|
|
244
|
+
depth--;
|
|
245
|
+
typeTokens.push(t);
|
|
246
|
+
j++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Delimiters that end the type at depth 0
|
|
253
|
+
if (depth === 0) {
|
|
254
|
+
if (tTag === '=' || tTag === 'REACTIVE_ASSIGN' ||
|
|
255
|
+
tTag === 'COMPUTED_ASSIGN' || tTag === 'READONLY_ASSIGN' ||
|
|
256
|
+
tTag === 'REACT_ASSIGN' || tTag === 'TERMINATOR' ||
|
|
257
|
+
tTag === 'INDENT' || tTag === 'OUTDENT' ||
|
|
258
|
+
tTag === '->' || tTag === ',') {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// => at depth 0: function type arrow, continue collecting
|
|
264
|
+
// -> at depth 0: code arrow, handled as delimiter above
|
|
265
|
+
typeTokens.push(t);
|
|
266
|
+
j++;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return typeTokens;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Build a clean type string from collected tokens
|
|
273
|
+
function buildTypeString(typeTokens) {
|
|
274
|
+
if (typeTokens.length === 0) return '';
|
|
275
|
+
let typeStr = typeTokens.map(t => t[1]).join(' ').replace(/\s+/g, ' ').trim();
|
|
276
|
+
typeStr = typeStr
|
|
277
|
+
.replace(/\s*<\s*/g, '<').replace(/\s*>\s*/g, '>')
|
|
278
|
+
.replace(/\s*\[\s*/g, '[').replace(/\s*\]\s*/g, ']')
|
|
279
|
+
.replace(/\s*\(\s*/g, '(').replace(/\s*\)\s*/g, ')')
|
|
280
|
+
.replace(/\s*,\s*/g, ', ');
|
|
281
|
+
return typeStr;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Collect balanced angle brackets starting at position j (the < token)
|
|
285
|
+
function collectBalancedAngles(tokens, j) {
|
|
286
|
+
if (j >= tokens.length) return null;
|
|
287
|
+
let t = tokens[j];
|
|
288
|
+
if (t[0] !== 'COMPARE' || t[1] !== '<') return null;
|
|
289
|
+
|
|
290
|
+
let collected = [t];
|
|
291
|
+
let depth = 1;
|
|
292
|
+
let k = j + 1;
|
|
293
|
+
|
|
294
|
+
while (k < tokens.length && depth > 0) {
|
|
295
|
+
let tk = tokens[k];
|
|
296
|
+
collected.push(tk);
|
|
297
|
+
if (tk[0] === 'COMPARE' && tk[1] === '<') depth++;
|
|
298
|
+
else if (tk[0] === 'COMPARE' && tk[1] === '>') depth--;
|
|
299
|
+
k++;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return depth === 0 ? collected : null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Collect structural type body: { prop: type; ... }
|
|
306
|
+
function collectStructuralType(tokens, indentIdx) {
|
|
307
|
+
let props = [];
|
|
308
|
+
let j = indentIdx + 1; // skip INDENT
|
|
309
|
+
let depth = 1;
|
|
310
|
+
|
|
311
|
+
while (j < tokens.length && depth > 0) {
|
|
312
|
+
let t = tokens[j];
|
|
313
|
+
if (t[0] === 'INDENT') { depth++; j++; continue; }
|
|
314
|
+
if (t[0] === 'OUTDENT') {
|
|
315
|
+
depth--;
|
|
316
|
+
if (depth === 0) break;
|
|
317
|
+
j++;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (t[0] === 'TERMINATOR') { j++; continue; }
|
|
321
|
+
|
|
322
|
+
// Collect a property line: name (? optional) : type
|
|
323
|
+
// Property tokens can be PROPERTY or IDENTIFIER
|
|
324
|
+
if (depth === 1 && (t[0] === 'PROPERTY' || t[0] === 'IDENTIFIER')) {
|
|
325
|
+
let propName = t[1];
|
|
326
|
+
let optional = false;
|
|
327
|
+
let readonly = false;
|
|
328
|
+
j++;
|
|
329
|
+
|
|
330
|
+
// Check for readonly prefix
|
|
331
|
+
if (propName === 'readonly' && (tokens[j]?.[0] === 'PROPERTY' || tokens[j]?.[0] === 'IDENTIFIER')) {
|
|
332
|
+
readonly = true;
|
|
333
|
+
propName = tokens[j][1];
|
|
334
|
+
// Carry predicate flag through
|
|
335
|
+
if (tokens[j].data?.predicate) optional = true;
|
|
336
|
+
j++;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check for ? (optional property) — lexer stores as .data.predicate
|
|
340
|
+
if (t.data?.predicate) optional = true;
|
|
341
|
+
// Also check for standalone ? token
|
|
342
|
+
if (tokens[j]?.[1] === '?' && !tokens[j]?.spaced) {
|
|
343
|
+
optional = true;
|
|
344
|
+
j++;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Skip : separator
|
|
348
|
+
if (tokens[j]?.[1] === ':') j++;
|
|
349
|
+
|
|
350
|
+
// Collect the type (until TERMINATOR, OUTDENT, or next property)
|
|
351
|
+
let propTypeTokens = [];
|
|
352
|
+
while (j < tokens.length) {
|
|
353
|
+
let pt = tokens[j];
|
|
354
|
+
if (pt[0] === 'TERMINATOR' || pt[0] === 'OUTDENT' || pt[0] === 'INDENT') break;
|
|
355
|
+
propTypeTokens.push(pt);
|
|
356
|
+
j++;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let typeStr = buildTypeString(propTypeTokens);
|
|
360
|
+
let prefix = readonly ? 'readonly ' : '';
|
|
361
|
+
let optMark = optional ? '?' : '';
|
|
362
|
+
props.push(`${prefix}${propName}${optMark}: ${typeStr}`);
|
|
363
|
+
} else {
|
|
364
|
+
j++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return '{ ' + props.join('; ') + ' }';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Find the matching OUTDENT for an INDENT at position idx
|
|
372
|
+
function findMatchingOutdent(tokens, idx) {
|
|
373
|
+
let depth = 0;
|
|
374
|
+
for (let j = idx; j < tokens.length; j++) {
|
|
375
|
+
if (tokens[j][0] === 'INDENT') depth++;
|
|
376
|
+
if (tokens[j][0] === 'OUTDENT') {
|
|
377
|
+
depth--;
|
|
378
|
+
if (depth === 0) return j;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return tokens.length - 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Collect block union members: | "a" | "b" | "c"
|
|
385
|
+
function collectBlockUnion(tokens, startIdx) {
|
|
386
|
+
let j = startIdx;
|
|
387
|
+
|
|
388
|
+
// Skip TERMINATOR if present
|
|
389
|
+
if (tokens[j]?.[0] === 'TERMINATOR') j++;
|
|
390
|
+
|
|
391
|
+
// Need INDENT for block form
|
|
392
|
+
if (tokens[j]?.[0] !== 'INDENT') return null;
|
|
393
|
+
|
|
394
|
+
let indentIdx = j;
|
|
395
|
+
j++;
|
|
396
|
+
|
|
397
|
+
// Check if first non-terminator token is |
|
|
398
|
+
while (j < tokens.length && tokens[j][0] === 'TERMINATOR') j++;
|
|
399
|
+
if (!tokens[j] || tokens[j][1] !== '|') return null;
|
|
400
|
+
|
|
401
|
+
let members = [];
|
|
402
|
+
let depth = 1;
|
|
403
|
+
j = indentIdx + 1;
|
|
404
|
+
|
|
405
|
+
while (j < tokens.length && depth > 0) {
|
|
406
|
+
let t = tokens[j];
|
|
407
|
+
if (t[0] === 'INDENT') { depth++; j++; continue; }
|
|
408
|
+
if (t[0] === 'OUTDENT') {
|
|
409
|
+
depth--;
|
|
410
|
+
if (depth === 0) break;
|
|
411
|
+
j++;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (t[0] === 'TERMINATOR') { j++; continue; }
|
|
415
|
+
|
|
416
|
+
// Skip leading |
|
|
417
|
+
if (t[1] === '|' && depth === 1) {
|
|
418
|
+
j++;
|
|
419
|
+
// Collect member tokens until next | or TERMINATOR
|
|
420
|
+
let memberTokens = [];
|
|
421
|
+
while (j < tokens.length) {
|
|
422
|
+
let mt = tokens[j];
|
|
423
|
+
if (mt[0] === 'TERMINATOR' || mt[0] === 'OUTDENT' ||
|
|
424
|
+
(mt[1] === '|' && depth === 1)) break;
|
|
425
|
+
memberTokens.push(mt);
|
|
426
|
+
j++;
|
|
427
|
+
}
|
|
428
|
+
if (memberTokens.length > 0) {
|
|
429
|
+
members.push(buildTypeString(memberTokens));
|
|
430
|
+
}
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
j++;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (members.length === 0) return null;
|
|
438
|
+
|
|
439
|
+
let endIdx = findMatchingOutdent(tokens, indentIdx);
|
|
440
|
+
return { typeText: members.join(' | '), endIdx };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// emitTypes — generate .d.ts from annotated token stream
|
|
445
|
+
// ============================================================================
|
|
446
|
+
|
|
447
|
+
export function emitTypes(tokens) {
|
|
448
|
+
let lines = [];
|
|
449
|
+
let indentLevel = 0;
|
|
450
|
+
let indentStr = ' ';
|
|
451
|
+
let indent = () => indentStr.repeat(indentLevel);
|
|
452
|
+
let inClass = false;
|
|
453
|
+
|
|
454
|
+
// Format { prop; prop } into multi-line block
|
|
455
|
+
let emitBlock = (prefix, body, suffix) => {
|
|
456
|
+
if (body.startsWith('{ ') && body.endsWith(' }')) {
|
|
457
|
+
let props = body.slice(2, -2).split('; ').filter(p => p.trim());
|
|
458
|
+
if (props.length > 0) {
|
|
459
|
+
lines.push(`${indent()}${prefix}{`);
|
|
460
|
+
indentLevel++;
|
|
461
|
+
for (let prop of props) lines.push(`${indent()}${prop};`);
|
|
462
|
+
indentLevel--;
|
|
463
|
+
lines.push(`${indent()}}${suffix}`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
lines.push(`${indent()}${prefix}${body}${suffix}`);
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
471
|
+
let t = tokens[i];
|
|
472
|
+
let tag = t[0];
|
|
473
|
+
|
|
474
|
+
// Track export flag
|
|
475
|
+
let exported = false;
|
|
476
|
+
if (tag === 'EXPORT') {
|
|
477
|
+
exported = true;
|
|
478
|
+
i++;
|
|
479
|
+
if (i >= tokens.length) break;
|
|
480
|
+
t = tokens[i];
|
|
481
|
+
tag = t[0];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// TYPE_DECL marker — emit type alias or interface
|
|
485
|
+
if (tag === 'TYPE_DECL') {
|
|
486
|
+
let data = t.data;
|
|
487
|
+
if (!data) continue;
|
|
488
|
+
let exp = (exported || data.exported) ? 'export ' : '';
|
|
489
|
+
let params = data.typeParams || '';
|
|
490
|
+
|
|
491
|
+
if (data.kind === 'interface') {
|
|
492
|
+
let ext = data.extends ? ` extends ${data.extends}` : '';
|
|
493
|
+
emitBlock(`${exp}interface ${data.name}${params}${ext} `, data.typeText || '{}', '');
|
|
494
|
+
} else {
|
|
495
|
+
let typeText = expandSuffixes(data.typeText || '');
|
|
496
|
+
emitBlock(`${exp}type ${data.name}${params} = `, typeText, ';');
|
|
497
|
+
}
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ENUM — emit enum declaration for .d.ts
|
|
502
|
+
if (tag === 'ENUM') {
|
|
503
|
+
let exp = exported ? 'export ' : '';
|
|
504
|
+
let nameToken = tokens[i + 1];
|
|
505
|
+
if (!nameToken) continue;
|
|
506
|
+
let enumName = nameToken[1];
|
|
507
|
+
|
|
508
|
+
// Find INDENT ... OUTDENT for enum body
|
|
509
|
+
let j = i + 2;
|
|
510
|
+
if (tokens[j]?.[0] === 'INDENT') {
|
|
511
|
+
lines.push(`${indent()}${exp}enum ${enumName} {`);
|
|
512
|
+
indentLevel++;
|
|
513
|
+
j++;
|
|
514
|
+
let members = [];
|
|
515
|
+
|
|
516
|
+
while (j < tokens.length && tokens[j][0] !== 'OUTDENT') {
|
|
517
|
+
if (tokens[j][0] === 'TERMINATOR') { j++; continue; }
|
|
518
|
+
if (tokens[j][0] === 'IDENTIFIER') {
|
|
519
|
+
let memberName = tokens[j][1];
|
|
520
|
+
j++;
|
|
521
|
+
if (tokens[j]?.[1] === '=') {
|
|
522
|
+
j++;
|
|
523
|
+
let val = tokens[j]?.[1];
|
|
524
|
+
members.push(`${memberName} = ${val}`);
|
|
525
|
+
j++;
|
|
526
|
+
} else {
|
|
527
|
+
members.push(memberName);
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
j++;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
for (let m = 0; m < members.length; m++) {
|
|
535
|
+
let comma = m < members.length - 1 ? ',' : '';
|
|
536
|
+
lines.push(`${indent()}${members[m]}${comma}`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
indentLevel--;
|
|
540
|
+
lines.push(`${indent()}}`);
|
|
541
|
+
}
|
|
542
|
+
// Don't advance i — the parser still needs to see ENUM tokens
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// CLASS — emit class declaration
|
|
547
|
+
if (tag === 'CLASS') {
|
|
548
|
+
let exp = exported ? 'export ' : '';
|
|
549
|
+
let classNameToken = tokens[i + 1];
|
|
550
|
+
if (!classNameToken) continue;
|
|
551
|
+
let className = classNameToken[1];
|
|
552
|
+
|
|
553
|
+
// Check for extends
|
|
554
|
+
let ext = '';
|
|
555
|
+
let j = i + 2;
|
|
556
|
+
if (tokens[j]?.[0] === 'EXTENDS') {
|
|
557
|
+
ext = ` extends ${tokens[j + 1]?.[1] || ''}`;
|
|
558
|
+
j += 2;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Only emit if there are typed members
|
|
562
|
+
if (tokens[j]?.[0] === 'INDENT') {
|
|
563
|
+
let hasTypedMembers = false;
|
|
564
|
+
let k = j + 1;
|
|
565
|
+
while (k < tokens.length && tokens[k][0] !== 'OUTDENT') {
|
|
566
|
+
if (tokens[k].data?.type || tokens[k].data?.returnType) {
|
|
567
|
+
hasTypedMembers = true;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
k++;
|
|
571
|
+
}
|
|
572
|
+
if (hasTypedMembers) {
|
|
573
|
+
lines.push(`${indent()}${exp}declare class ${className}${ext} {`);
|
|
574
|
+
inClass = true;
|
|
575
|
+
indentLevel++;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// DEF — emit function declaration
|
|
582
|
+
if (tag === 'DEF') {
|
|
583
|
+
let nameToken = tokens[i + 1];
|
|
584
|
+
if (!nameToken) continue;
|
|
585
|
+
let fnName = nameToken[1];
|
|
586
|
+
let returnType = nameToken.data?.returnType;
|
|
587
|
+
let typeParams = nameToken.data?.typeParams || '';
|
|
588
|
+
|
|
589
|
+
// Collect parameters
|
|
590
|
+
let j = i + 2;
|
|
591
|
+
let params = [];
|
|
592
|
+
if (tokens[j]?.[0] === 'CALL_START') {
|
|
593
|
+
j++;
|
|
594
|
+
while (j < tokens.length && tokens[j][0] !== 'CALL_END') {
|
|
595
|
+
if (tokens[j][0] === 'IDENTIFIER') {
|
|
596
|
+
let paramName = tokens[j][1];
|
|
597
|
+
let paramType = tokens[j].data?.type;
|
|
598
|
+
if (paramType) {
|
|
599
|
+
params.push(`${paramName}: ${expandSuffixes(paramType)}`);
|
|
600
|
+
} else {
|
|
601
|
+
params.push(paramName);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
j++;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Only emit if there are type annotations
|
|
609
|
+
if (returnType || params.some(p => p.includes(':'))) {
|
|
610
|
+
let exp = exported ? 'export ' : '';
|
|
611
|
+
let declare = inClass ? '' : (exported ? '' : 'declare ');
|
|
612
|
+
let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
|
|
613
|
+
let paramStr = params.join(', ');
|
|
614
|
+
lines.push(`${indent()}${exp}${declare}function ${fnName}${typeParams}(${paramStr})${ret};`);
|
|
615
|
+
}
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Track INDENT/OUTDENT for class body
|
|
620
|
+
if (tag === 'INDENT') {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (tag === 'OUTDENT') {
|
|
624
|
+
if (inClass) {
|
|
625
|
+
indentLevel--;
|
|
626
|
+
lines.push(`${indent()}}`);
|
|
627
|
+
inClass = false;
|
|
628
|
+
}
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Variable assignments with type annotations
|
|
633
|
+
if (tag === 'IDENTIFIER' && t.data?.type) {
|
|
634
|
+
let varName = t[1];
|
|
635
|
+
let type = expandSuffixes(t.data.type);
|
|
636
|
+
let next = tokens[i + 1];
|
|
637
|
+
|
|
638
|
+
if (next) {
|
|
639
|
+
let exp = exported ? 'export ' : '';
|
|
640
|
+
let declare = exported ? '' : 'declare ';
|
|
641
|
+
|
|
642
|
+
if (next[0] === 'READONLY_ASSIGN') {
|
|
643
|
+
lines.push(`${indent()}${exp}${declare}const ${varName}: ${type};`);
|
|
644
|
+
} else if (next[0] === 'REACTIVE_ASSIGN') {
|
|
645
|
+
lines.push(`${indent()}${exp}${declare}const ${varName}: Signal<${type}>;`);
|
|
646
|
+
} else if (next[0] === 'COMPUTED_ASSIGN') {
|
|
647
|
+
lines.push(`${indent()}${exp}${declare}const ${varName}: Computed<${type}>;`);
|
|
648
|
+
} else if (next[0] === '=') {
|
|
649
|
+
if (inClass) {
|
|
650
|
+
lines.push(`${indent()}${varName}: ${type};`);
|
|
651
|
+
} else {
|
|
652
|
+
lines.push(`${indent()}${exp}let ${varName}: ${type};`);
|
|
653
|
+
}
|
|
654
|
+
} else if (inClass) {
|
|
655
|
+
// Class property without assignment
|
|
656
|
+
lines.push(`${indent()}${varName}: ${type};`);
|
|
657
|
+
}
|
|
658
|
+
} else if (inClass) {
|
|
659
|
+
lines.push(`${indent()}${varName}: ${type};`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (lines.length === 0) return null;
|
|
665
|
+
return lines.join('\n') + '\n';
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ============================================================================
|
|
669
|
+
// Suffix expansion — Rip type suffixes to TypeScript
|
|
670
|
+
// ============================================================================
|
|
671
|
+
|
|
672
|
+
function expandSuffixes(typeStr) {
|
|
673
|
+
if (!typeStr) return typeStr;
|
|
674
|
+
|
|
675
|
+
// Convert :: to : (annotation sigil to type separator)
|
|
676
|
+
typeStr = typeStr.replace(/::/g, ':');
|
|
677
|
+
|
|
678
|
+
// T?? → T | null | undefined
|
|
679
|
+
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?\?/g, '$1 | null | undefined');
|
|
680
|
+
|
|
681
|
+
// T? → T | undefined (but not ?. or ?: which are different)
|
|
682
|
+
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?(?![.:])/g, '$1 | undefined');
|
|
683
|
+
|
|
684
|
+
// T! → NonNullable<T>
|
|
685
|
+
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\!/g, 'NonNullable<$1>');
|
|
686
|
+
|
|
687
|
+
return typeStr;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ============================================================================
|
|
691
|
+
// generateEnum — runtime JavaScript enum object (CodeGenerator method)
|
|
692
|
+
// ============================================================================
|
|
693
|
+
|
|
694
|
+
export function generateEnum(head, rest, context) {
|
|
695
|
+
let [name, body] = rest;
|
|
696
|
+
let enumName = name?.valueOf?.() ?? name;
|
|
697
|
+
|
|
698
|
+
// Parse enum body from s-expression
|
|
699
|
+
let pairs = [];
|
|
700
|
+
if (Array.isArray(body)) {
|
|
701
|
+
let items = body[0] === 'block' ? body.slice(1) : [body];
|
|
702
|
+
for (let item of items) {
|
|
703
|
+
if (Array.isArray(item)) {
|
|
704
|
+
if (item[0]?.valueOf?.() === '=') {
|
|
705
|
+
let key = item[1]?.valueOf?.() ?? item[1];
|
|
706
|
+
let val = item[2]?.valueOf?.() ?? item[2];
|
|
707
|
+
pairs.push([key, val]);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (pairs.length === 0) return `const ${enumName} = {}`;
|
|
714
|
+
|
|
715
|
+
let forward = pairs.map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
716
|
+
let reverse = pairs.map(([k, v]) => `${v}: "${k}"`).join(', ');
|
|
717
|
+
return `const ${enumName} = {${forward}, ${reverse}}`;
|
|
718
|
+
}
|