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/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
+ }