rip-lang 3.6.0 → 3.6.2
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 +1 -1
- package/docs/RIP-GUIDE.md +51 -0
- package/docs/RIP-TYPES.md +98 -0
- package/docs/dist/rip.browser.js +611 -272
- package/docs/dist/rip.browser.min.js +170 -170
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/docs/index.html +35 -27
- package/package.json +1 -1
- package/src/compiler.js +55 -43
- package/src/components.js +157 -159
- package/src/grammar/grammar.rip +4 -0
- package/src/parser.js +41 -41
- package/src/repl.js +4 -128
- package/src/types.js +416 -35
package/src/types.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// Type System — Optional type annotations and .d.ts emission for Rip
|
|
2
2
|
//
|
|
3
|
-
// Architecture:
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Architecture:
|
|
4
|
+
// installTypeSupport(Lexer) — adds rewriteTypes() to the lexer prototype.
|
|
5
|
+
// Strips type annotations from the token stream and stores them as
|
|
6
|
+
// metadata on surviving tokens. The parser never sees types.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// emitTypes(tokens, sexpr) — generates .d.ts from annotated tokens and
|
|
9
|
+
// the parsed s-expression tree. Called after parsing so it has access
|
|
10
|
+
// to both token-level annotations (variables, functions, types) and
|
|
11
|
+
// s-expression structures (components). One function, one output.
|
|
12
|
+
//
|
|
13
|
+
// generateEnum() — the one CodeGenerator method for runtime enum output.
|
|
14
|
+
// Enums cross into the grammar because they emit runtime JavaScript.
|
|
11
15
|
|
|
12
16
|
// ============================================================================
|
|
13
17
|
// installTypeSupport — adds rewriteTypes() to Lexer.prototype
|
|
@@ -56,7 +60,7 @@ export function installTypeSupport(Lexer) {
|
|
|
56
60
|
let isAlias = !isDef && tokens[i + 1 + genTokens.length]?.[0] === 'TYPE_ALIAS';
|
|
57
61
|
if (isDef || isAlias) {
|
|
58
62
|
if (!token.data) token.data = {};
|
|
59
|
-
token.data.typeParams = genTokens
|
|
63
|
+
token.data.typeParams = buildTypeString(genTokens);
|
|
60
64
|
tokens.splice(i + 1, genTokens.length);
|
|
61
65
|
// After removing <T>, retag ( as CALL_START if it follows DEF IDENTIFIER
|
|
62
66
|
if (isDef && tokens[i + 1]?.[0] === '(') {
|
|
@@ -272,12 +276,17 @@ function collectTypeExpression(tokens, j) {
|
|
|
272
276
|
// Build a clean type string from collected tokens
|
|
273
277
|
function buildTypeString(typeTokens) {
|
|
274
278
|
if (typeTokens.length === 0) return '';
|
|
279
|
+
// Bare => (no params) means () => — add empty parens
|
|
280
|
+
if (typeTokens[0]?.[0] === '=>') typeTokens.unshift(['', '()']);
|
|
275
281
|
let typeStr = typeTokens.map(t => t[1]).join(' ').replace(/\s+/g, ' ').trim();
|
|
276
282
|
typeStr = typeStr
|
|
277
283
|
.replace(/\s*<\s*/g, '<').replace(/\s*>\s*/g, '>')
|
|
278
284
|
.replace(/\s*\[\s*/g, '[').replace(/\s*\]\s*/g, ']')
|
|
279
285
|
.replace(/\s*\(\s*/g, '(').replace(/\s*\)\s*/g, ')')
|
|
280
|
-
.replace(/\s*,\s*/g, ', ')
|
|
286
|
+
.replace(/\s*,\s*/g, ', ')
|
|
287
|
+
.replace(/\s*=>\s*/g, ' => ')
|
|
288
|
+
.replace(/ :: /g, ': ')
|
|
289
|
+
.replace(/:: /g, ': ');
|
|
281
290
|
return typeStr;
|
|
282
291
|
}
|
|
283
292
|
|
|
@@ -347,11 +356,17 @@ function collectStructuralType(tokens, indentIdx) {
|
|
|
347
356
|
// Skip : separator
|
|
348
357
|
if (tokens[j]?.[1] === ':') j++;
|
|
349
358
|
|
|
350
|
-
// Collect the type (until TERMINATOR
|
|
359
|
+
// Collect the type (until TERMINATOR or OUTDENT at property depth)
|
|
351
360
|
let propTypeTokens = [];
|
|
361
|
+
let typeDepth = 0;
|
|
352
362
|
while (j < tokens.length) {
|
|
353
363
|
let pt = tokens[j];
|
|
354
|
-
if (pt[0] === '
|
|
364
|
+
if (pt[0] === 'INDENT') { typeDepth++; j++; continue; }
|
|
365
|
+
if (pt[0] === 'OUTDENT') {
|
|
366
|
+
if (typeDepth > 0) { typeDepth--; j++; continue; }
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
if (pt[0] === 'TERMINATOR' && typeDepth === 0) break;
|
|
355
370
|
propTypeTokens.push(pt);
|
|
356
371
|
j++;
|
|
357
372
|
}
|
|
@@ -441,15 +456,17 @@ function collectBlockUnion(tokens, startIdx) {
|
|
|
441
456
|
}
|
|
442
457
|
|
|
443
458
|
// ============================================================================
|
|
444
|
-
// emitTypes — generate .d.ts from annotated
|
|
459
|
+
// emitTypes — generate .d.ts from annotated tokens + s-expression tree
|
|
445
460
|
// ============================================================================
|
|
446
461
|
|
|
447
|
-
export function emitTypes(tokens) {
|
|
462
|
+
export function emitTypes(tokens, sexpr = null) {
|
|
448
463
|
let lines = [];
|
|
449
464
|
let indentLevel = 0;
|
|
450
465
|
let indentStr = ' ';
|
|
451
466
|
let indent = () => indentStr.repeat(indentLevel);
|
|
452
467
|
let inClass = false;
|
|
468
|
+
let usesSignal = false;
|
|
469
|
+
let usesComputed = false;
|
|
453
470
|
|
|
454
471
|
// Format { prop; prop } into multi-line block
|
|
455
472
|
let emitBlock = (prefix, body, suffix) => {
|
|
@@ -467,6 +484,110 @@ export function emitTypes(tokens) {
|
|
|
467
484
|
lines.push(`${indent()}${prefix}${body}${suffix}`);
|
|
468
485
|
};
|
|
469
486
|
|
|
487
|
+
// Collect function parameters (handles simple, destructured, rest, defaults)
|
|
488
|
+
let collectParams = (tokens, startIdx) => {
|
|
489
|
+
let params = [];
|
|
490
|
+
let j = startIdx;
|
|
491
|
+
let openTag = tokens[j]?.[0];
|
|
492
|
+
if (openTag !== 'CALL_START' && openTag !== 'PARAM_START') return { params, endIdx: j };
|
|
493
|
+
let closeTag = openTag === 'CALL_START' ? 'CALL_END' : 'PARAM_END';
|
|
494
|
+
j++;
|
|
495
|
+
let depth = 0;
|
|
496
|
+
|
|
497
|
+
while (j < tokens.length && !(tokens[j][0] === closeTag && depth === 0)) {
|
|
498
|
+
let tok = tokens[j];
|
|
499
|
+
|
|
500
|
+
// Skip commas at depth 0
|
|
501
|
+
if (tok[1] === ',' && depth === 0) { j++; continue; }
|
|
502
|
+
|
|
503
|
+
// Track nesting
|
|
504
|
+
if (tok[0] === '{' || tok[0] === '[' || tok[0] === 'CALL_START' ||
|
|
505
|
+
tok[0] === 'PARAM_START' || tok[0] === 'INDEX_START') depth++;
|
|
506
|
+
if (tok[0] === '}' || tok[0] === ']' || tok[0] === 'CALL_END' ||
|
|
507
|
+
tok[0] === 'PARAM_END' || tok[0] === 'INDEX_END') { depth--; j++; continue; }
|
|
508
|
+
|
|
509
|
+
// @ prefix (constructor shorthand: @name)
|
|
510
|
+
if (tok[0] === '@') {
|
|
511
|
+
j++;
|
|
512
|
+
if (tokens[j]?.[0] === 'PROPERTY' || tokens[j]?.[0] === 'IDENTIFIER') {
|
|
513
|
+
let name = tokens[j][1];
|
|
514
|
+
let type = tokens[j].data?.type;
|
|
515
|
+
params.push(type ? `${name}: ${expandSuffixes(type)}` : name);
|
|
516
|
+
j++;
|
|
517
|
+
}
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Rest parameter: ...name
|
|
522
|
+
if (tok[0] === 'SPREAD' || tok[1] === '...') {
|
|
523
|
+
j++;
|
|
524
|
+
if (tokens[j]?.[0] === 'IDENTIFIER') {
|
|
525
|
+
let name = tokens[j][1];
|
|
526
|
+
let type = tokens[j].data?.type;
|
|
527
|
+
params.push(type ? `...${name}: ${expandSuffixes(type)}` : `...${name}: any[]`);
|
|
528
|
+
j++;
|
|
529
|
+
}
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Destructured object parameter: { a, b }
|
|
534
|
+
if (tok[0] === '{') {
|
|
535
|
+
// Collect the whole destructured pattern as a string
|
|
536
|
+
let pattern = '{';
|
|
537
|
+
j++;
|
|
538
|
+
let d = 1;
|
|
539
|
+
while (j < tokens.length && d > 0) {
|
|
540
|
+
if (tokens[j][0] === '{') d++;
|
|
541
|
+
if (tokens[j][0] === '}') d--;
|
|
542
|
+
if (d > 0) pattern += tokens[j][1] + (tokens[j + 1]?.[0] === '}' ? '' : ', ');
|
|
543
|
+
j++;
|
|
544
|
+
}
|
|
545
|
+
pattern += '}';
|
|
546
|
+
// Check if the closing } had a type annotation
|
|
547
|
+
let type = tokens[j - 1]?.data?.type;
|
|
548
|
+
params.push(type ? `${pattern}: ${expandSuffixes(type)}` : pattern);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Simple identifier parameter
|
|
553
|
+
if (tok[0] === 'IDENTIFIER') {
|
|
554
|
+
let paramName = tok[1];
|
|
555
|
+
let paramType = tok.data?.type;
|
|
556
|
+
|
|
557
|
+
// Check for default value (skip = and the default expression)
|
|
558
|
+
let hasDefault = false;
|
|
559
|
+
if (tokens[j + 1]?.[0] === '=') {
|
|
560
|
+
hasDefault = true;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (paramType) {
|
|
564
|
+
params.push(`${paramName}${hasDefault ? '?' : ''}: ${expandSuffixes(paramType)}`);
|
|
565
|
+
} else {
|
|
566
|
+
params.push(paramName);
|
|
567
|
+
}
|
|
568
|
+
j++;
|
|
569
|
+
|
|
570
|
+
// Skip past default value expression
|
|
571
|
+
if (hasDefault) {
|
|
572
|
+
j++; // skip =
|
|
573
|
+
let dd = 0;
|
|
574
|
+
while (j < tokens.length) {
|
|
575
|
+
let dt = tokens[j];
|
|
576
|
+
if (dt[0] === '(' || dt[0] === '[' || dt[0] === '{') dd++;
|
|
577
|
+
if (dt[0] === ')' || dt[0] === ']' || dt[0] === '}') dd--;
|
|
578
|
+
if (dd === 0 && (dt[1] === ',' || dt[0] === 'CALL_END')) break;
|
|
579
|
+
j++;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
j++;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return { params, endIdx: j };
|
|
589
|
+
};
|
|
590
|
+
|
|
470
591
|
for (let i = 0; i < tokens.length; i++) {
|
|
471
592
|
let t = tokens[i];
|
|
472
593
|
let tag = t[0];
|
|
@@ -479,6 +600,40 @@ export function emitTypes(tokens) {
|
|
|
479
600
|
if (i >= tokens.length) break;
|
|
480
601
|
t = tokens[i];
|
|
481
602
|
tag = t[0];
|
|
603
|
+
|
|
604
|
+
// Export default
|
|
605
|
+
if (tag === 'DEFAULT') {
|
|
606
|
+
i++;
|
|
607
|
+
if (i >= tokens.length) break;
|
|
608
|
+
t = tokens[i];
|
|
609
|
+
tag = t[0];
|
|
610
|
+
|
|
611
|
+
// export default IDENTIFIER (re-export)
|
|
612
|
+
if (tag === 'IDENTIFIER') {
|
|
613
|
+
lines.push(`${indent()}export default ${t[1]};`);
|
|
614
|
+
}
|
|
615
|
+
// export default { ... } or other expressions — skip for now
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Import statements — pass through for type references
|
|
621
|
+
if (tag === 'IMPORT') {
|
|
622
|
+
let importTokens = [];
|
|
623
|
+
let j = i + 1;
|
|
624
|
+
while (j < tokens.length && tokens[j][0] !== 'TERMINATOR') {
|
|
625
|
+
importTokens.push(tokens[j]);
|
|
626
|
+
j++;
|
|
627
|
+
}
|
|
628
|
+
// Reconstruct: join with spaces, then clean up spacing
|
|
629
|
+
let raw = 'import ' + importTokens.map(tk => tk[1]).join(' ');
|
|
630
|
+
raw = raw.replace(/\s+/g, ' ')
|
|
631
|
+
.replace(/\s*,\s*/g, ', ')
|
|
632
|
+
.replace(/\{\s*/g, '{ ').replace(/\s*\}/g, ' }')
|
|
633
|
+
.trim();
|
|
634
|
+
lines.push(`${indent()}${raw};`);
|
|
635
|
+
i = j;
|
|
636
|
+
continue;
|
|
482
637
|
}
|
|
483
638
|
|
|
484
639
|
// TYPE_DECL marker — emit type alias or interface
|
|
@@ -578,7 +733,7 @@ export function emitTypes(tokens) {
|
|
|
578
733
|
continue;
|
|
579
734
|
}
|
|
580
735
|
|
|
581
|
-
// DEF — emit function declaration
|
|
736
|
+
// DEF — emit function or method declaration
|
|
582
737
|
if (tag === 'DEF') {
|
|
583
738
|
let nameToken = tokens[i + 1];
|
|
584
739
|
if (!nameToken) continue;
|
|
@@ -586,24 +741,7 @@ export function emitTypes(tokens) {
|
|
|
586
741
|
let returnType = nameToken.data?.returnType;
|
|
587
742
|
let typeParams = nameToken.data?.typeParams || '';
|
|
588
743
|
|
|
589
|
-
|
|
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
|
-
}
|
|
744
|
+
let { params, endIdx } = collectParams(tokens, i + 2);
|
|
607
745
|
|
|
608
746
|
// Only emit if there are type annotations
|
|
609
747
|
if (returnType || params.some(p => p.includes(':'))) {
|
|
@@ -611,11 +749,71 @@ export function emitTypes(tokens) {
|
|
|
611
749
|
let declare = inClass ? '' : (exported ? '' : 'declare ');
|
|
612
750
|
let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
|
|
613
751
|
let paramStr = params.join(', ');
|
|
614
|
-
|
|
752
|
+
if (inClass) {
|
|
753
|
+
lines.push(`${indent()}${fnName}${typeParams}(${paramStr})${ret};`);
|
|
754
|
+
} else {
|
|
755
|
+
lines.push(`${indent()}${exp}${declare}function ${fnName}${typeParams}(${paramStr})${ret};`);
|
|
756
|
+
}
|
|
615
757
|
}
|
|
616
758
|
continue;
|
|
617
759
|
}
|
|
618
760
|
|
|
761
|
+
// Class method block: { PROPERTY ... PROPERTY ... }
|
|
762
|
+
// Contains one or more methods separated by TERMINATOR
|
|
763
|
+
if (tag === '{' && inClass) {
|
|
764
|
+
let j = i + 1;
|
|
765
|
+
let braceDepth = 1;
|
|
766
|
+
|
|
767
|
+
while (j < tokens.length && braceDepth > 0) {
|
|
768
|
+
let tok = tokens[j];
|
|
769
|
+
|
|
770
|
+
if (tok[0] === '{') { braceDepth++; j++; continue; }
|
|
771
|
+
if (tok[0] === '}') { braceDepth--; j++; continue; }
|
|
772
|
+
if (tok[0] === 'TERMINATOR') { j++; continue; }
|
|
773
|
+
|
|
774
|
+
// Found a method: PROPERTY "name" : PARAM_START ... PARAM_END -> body
|
|
775
|
+
if (tok[0] === 'PROPERTY' && braceDepth === 1) {
|
|
776
|
+
let methodName = tok[1];
|
|
777
|
+
let returnType = tok.data?.returnType;
|
|
778
|
+
j++;
|
|
779
|
+
|
|
780
|
+
// Skip : separator
|
|
781
|
+
if (tokens[j]?.[1] === ':') j++;
|
|
782
|
+
|
|
783
|
+
let params = [];
|
|
784
|
+
if (tokens[j]?.[0] === 'PARAM_START') {
|
|
785
|
+
let result = collectParams(tokens, j);
|
|
786
|
+
params = result.params;
|
|
787
|
+
j = result.endIdx + 1;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Skip -> and method body (INDENT ... OUTDENT)
|
|
791
|
+
if (tokens[j]?.[0] === '->' || tokens[j]?.[0] === '=>') j++;
|
|
792
|
+
if (tokens[j]?.[0] === 'INDENT') {
|
|
793
|
+
let d = 1;
|
|
794
|
+
j++;
|
|
795
|
+
while (j < tokens.length && d > 0) {
|
|
796
|
+
if (tokens[j][0] === 'INDENT') d++;
|
|
797
|
+
if (tokens[j][0] === 'OUTDENT') d--;
|
|
798
|
+
j++;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (returnType || params.some(p => p.includes(':'))) {
|
|
803
|
+
let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
|
|
804
|
+
let paramStr = params.join(', ');
|
|
805
|
+
lines.push(`${indent()}${methodName}(${paramStr})${ret};`);
|
|
806
|
+
}
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
j++;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
i = j - 1;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
|
|
619
817
|
// Track INDENT/OUTDENT for class body
|
|
620
818
|
if (tag === 'INDENT') {
|
|
621
819
|
continue;
|
|
@@ -629,6 +827,35 @@ export function emitTypes(tokens) {
|
|
|
629
827
|
continue;
|
|
630
828
|
}
|
|
631
829
|
|
|
830
|
+
// Arrow function assignment: name = (params) -> body
|
|
831
|
+
if (tag === 'IDENTIFIER' && !inClass && tokens[i + 1]?.[0] === '=' &&
|
|
832
|
+
(tokens[i + 2]?.[0] === 'PARAM_START' || tokens[i + 2]?.[0] === '(')) {
|
|
833
|
+
let fnName = t[1];
|
|
834
|
+
let j = i + 2;
|
|
835
|
+
|
|
836
|
+
let { params } = collectParams(tokens, j);
|
|
837
|
+
|
|
838
|
+
// Find the -> or => token to get return type
|
|
839
|
+
let k = j;
|
|
840
|
+
let depth = 0;
|
|
841
|
+
while (k < tokens.length) {
|
|
842
|
+
if (tokens[k][0] === 'PARAM_START' || tokens[k][0] === '(') depth++;
|
|
843
|
+
if (tokens[k][0] === 'PARAM_END' || tokens[k][0] === ')') depth--;
|
|
844
|
+
if (depth === 0 && (tokens[k][0] === '->' || tokens[k][0] === '=>')) break;
|
|
845
|
+
k++;
|
|
846
|
+
}
|
|
847
|
+
let returnType = tokens[k]?.data?.returnType;
|
|
848
|
+
|
|
849
|
+
if (returnType || params.some(p => p.includes(':'))) {
|
|
850
|
+
let exp = exported ? 'export ' : '';
|
|
851
|
+
let declare = exported ? '' : 'declare ';
|
|
852
|
+
let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
|
|
853
|
+
let paramStr = params.join(', ');
|
|
854
|
+
lines.push(`${indent()}${exp}${declare}function ${fnName}(${paramStr})${ret};`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
632
859
|
// Variable assignments with type annotations
|
|
633
860
|
if (tag === 'IDENTIFIER' && t.data?.type) {
|
|
634
861
|
let varName = t[1];
|
|
@@ -642,11 +869,35 @@ export function emitTypes(tokens) {
|
|
|
642
869
|
if (next[0] === 'READONLY_ASSIGN') {
|
|
643
870
|
lines.push(`${indent()}${exp}${declare}const ${varName}: ${type};`);
|
|
644
871
|
} else if (next[0] === 'REACTIVE_ASSIGN') {
|
|
872
|
+
usesSignal = true;
|
|
645
873
|
lines.push(`${indent()}${exp}${declare}const ${varName}: Signal<${type}>;`);
|
|
646
874
|
} else if (next[0] === 'COMPUTED_ASSIGN') {
|
|
875
|
+
usesComputed = true;
|
|
647
876
|
lines.push(`${indent()}${exp}${declare}const ${varName}: Computed<${type}>;`);
|
|
877
|
+
} else if (next[0] === 'REACT_ASSIGN') {
|
|
878
|
+
lines.push(`${indent()}${exp}${declare}const ${varName}: () => void;`);
|
|
648
879
|
} else if (next[0] === '=') {
|
|
649
|
-
if
|
|
880
|
+
// Check if RHS is an arrow function with return type
|
|
881
|
+
let arrowIdx = i + 2;
|
|
882
|
+
// Skip past PARAM_START ... PARAM_END if present
|
|
883
|
+
if (tokens[arrowIdx]?.[0] === 'PARAM_START') {
|
|
884
|
+
let d = 1, k = arrowIdx + 1;
|
|
885
|
+
while (k < tokens.length && d > 0) {
|
|
886
|
+
if (tokens[k][0] === 'PARAM_START') d++;
|
|
887
|
+
if (tokens[k][0] === 'PARAM_END') d--;
|
|
888
|
+
k++;
|
|
889
|
+
}
|
|
890
|
+
arrowIdx = k;
|
|
891
|
+
}
|
|
892
|
+
let arrowToken = tokens[arrowIdx];
|
|
893
|
+
if (arrowToken && (arrowToken[0] === '->' || arrowToken[0] === '=>') &&
|
|
894
|
+
arrowToken.data?.returnType) {
|
|
895
|
+
// Typed arrow function assignment
|
|
896
|
+
let returnType = expandSuffixes(arrowToken.data.returnType);
|
|
897
|
+
let { params } = collectParams(tokens, i + 2);
|
|
898
|
+
let paramStr = params.join(', ');
|
|
899
|
+
lines.push(`${indent()}${exp}${declare}function ${varName}(${paramStr}): ${returnType};`);
|
|
900
|
+
} else if (inClass) {
|
|
650
901
|
lines.push(`${indent()}${varName}: ${type};`);
|
|
651
902
|
} else {
|
|
652
903
|
lines.push(`${indent()}${exp}let ${varName}: ${type};`);
|
|
@@ -661,8 +912,35 @@ export function emitTypes(tokens) {
|
|
|
661
912
|
}
|
|
662
913
|
}
|
|
663
914
|
|
|
915
|
+
// Walk s-expression tree for component declarations
|
|
916
|
+
let componentVars = new Set();
|
|
917
|
+
if (sexpr) {
|
|
918
|
+
emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars);
|
|
919
|
+
|
|
920
|
+
// Remove lines for variables that belong to components (emitted as class members)
|
|
921
|
+
if (componentVars.size > 0) {
|
|
922
|
+
for (let k = lines.length - 1; k >= 0; k--) {
|
|
923
|
+
let match = lines[k].match(/(?:declare |export )*(?:const|let) (\w+)/);
|
|
924
|
+
if (match && componentVars.has(match[1])) lines.splice(k, 1);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
664
929
|
if (lines.length === 0) return null;
|
|
665
|
-
|
|
930
|
+
|
|
931
|
+
// Prepend reactive type definitions if used
|
|
932
|
+
let preamble = [];
|
|
933
|
+
if (usesSignal) {
|
|
934
|
+
preamble.push('interface Signal<T> { value: T; read(): T; lock(): Signal<T>; free(): Signal<T>; kill(): T; }');
|
|
935
|
+
}
|
|
936
|
+
if (usesComputed) {
|
|
937
|
+
preamble.push('interface Computed<T> { readonly value: T; read(): T; lock(): Computed<T>; free(): Computed<T>; kill(): T; }');
|
|
938
|
+
}
|
|
939
|
+
if (preamble.length > 0) {
|
|
940
|
+
preamble.push('');
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return preamble.concat(lines).join('\n') + '\n';
|
|
666
944
|
}
|
|
667
945
|
|
|
668
946
|
// ============================================================================
|
|
@@ -687,6 +965,109 @@ function expandSuffixes(typeStr) {
|
|
|
687
965
|
return typeStr;
|
|
688
966
|
}
|
|
689
967
|
|
|
968
|
+
// ============================================================================
|
|
969
|
+
// Component type emission — walk s-expression for component declarations
|
|
970
|
+
// ============================================================================
|
|
971
|
+
|
|
972
|
+
function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars) {
|
|
973
|
+
if (!Array.isArray(sexpr)) return;
|
|
974
|
+
let head = sexpr[0]?.valueOf?.() ?? sexpr[0];
|
|
975
|
+
|
|
976
|
+
// export Name = component ... → ["export", ["=", "Name", ["component", ...members]]]
|
|
977
|
+
// Name = component ... → ["=", "Name", ["component", ...members]]
|
|
978
|
+
let exported = false;
|
|
979
|
+
let name = null;
|
|
980
|
+
let compNode = null;
|
|
981
|
+
|
|
982
|
+
if (head === 'export' && Array.isArray(sexpr[1])) {
|
|
983
|
+
exported = true;
|
|
984
|
+
let inner = sexpr[1];
|
|
985
|
+
let innerHead = inner[0]?.valueOf?.() ?? inner[0];
|
|
986
|
+
if (innerHead === '=' && Array.isArray(inner[2]) &&
|
|
987
|
+
(inner[2][0]?.valueOf?.() ?? inner[2][0]) === 'component') {
|
|
988
|
+
name = inner[1]?.valueOf?.() ?? inner[1];
|
|
989
|
+
compNode = inner[2];
|
|
990
|
+
}
|
|
991
|
+
} else if (head === '=' && Array.isArray(sexpr[2]) &&
|
|
992
|
+
(sexpr[2][0]?.valueOf?.() ?? sexpr[2][0]) === 'component') {
|
|
993
|
+
name = sexpr[1]?.valueOf?.() ?? sexpr[1];
|
|
994
|
+
compNode = sexpr[2];
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (name && compNode) {
|
|
998
|
+
let exp = exported ? 'export ' : '';
|
|
999
|
+
|
|
1000
|
+
// Component structure: ["component", parent, ["block", ...members]]
|
|
1001
|
+
let body = compNode[2];
|
|
1002
|
+
let members = (Array.isArray(body) && (body[0]?.valueOf?.() ?? body[0]) === 'block')
|
|
1003
|
+
? body.slice(1) : (body ? [body] : []);
|
|
1004
|
+
|
|
1005
|
+
let props = [];
|
|
1006
|
+
let methods = [];
|
|
1007
|
+
|
|
1008
|
+
for (let member of members) {
|
|
1009
|
+
if (!Array.isArray(member)) continue;
|
|
1010
|
+
let mHead = member[0]?.valueOf?.() ?? member[0];
|
|
1011
|
+
|
|
1012
|
+
// Reactive state: ["state", "count", 0]
|
|
1013
|
+
if (mHead === 'state') {
|
|
1014
|
+
let propName = member[1]?.valueOf?.() ?? member[1];
|
|
1015
|
+
let type = member[1]?.type;
|
|
1016
|
+
props.push(` ${propName}: ${type ? expandSuffixes(type) : 'any'};`);
|
|
1017
|
+
componentVars.add(propName);
|
|
1018
|
+
}
|
|
1019
|
+
// Computed: ["computed", "doubled", expr]
|
|
1020
|
+
else if (mHead === 'computed') {
|
|
1021
|
+
let propName = member[1]?.valueOf?.() ?? member[1];
|
|
1022
|
+
let type = member[1]?.type;
|
|
1023
|
+
props.push(` readonly ${propName}: ${type ? expandSuffixes(type) : 'any'};`);
|
|
1024
|
+
componentVars.add(propName);
|
|
1025
|
+
}
|
|
1026
|
+
// Method object: ["object", ["methodName", ["->", params, body], ":"]]
|
|
1027
|
+
else if (mHead === 'object') {
|
|
1028
|
+
for (let j = 1; j < member.length; j++) {
|
|
1029
|
+
let entry = member[j];
|
|
1030
|
+
if (!Array.isArray(entry)) continue;
|
|
1031
|
+
let methodName = entry[0]?.valueOf?.() ?? entry[0];
|
|
1032
|
+
if (methodName === 'render') continue; // skip render
|
|
1033
|
+
let fn = entry[1];
|
|
1034
|
+
if (Array.isArray(fn)) {
|
|
1035
|
+
let fnHead = fn[0]?.valueOf?.() ?? fn[0];
|
|
1036
|
+
if (fnHead === '->' || fnHead === '=>') {
|
|
1037
|
+
methods.push(` ${methodName}(): void;`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// Skip render blocks
|
|
1043
|
+
else if (mHead === 'render') {
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
lines.push(`${exp}declare class ${name} {`);
|
|
1049
|
+
lines.push(` constructor(props?: Record<string, any>);`);
|
|
1050
|
+
for (let p of props) lines.push(p);
|
|
1051
|
+
for (let m of methods) lines.push(m);
|
|
1052
|
+
lines.push(` mount(target: Element | string): ${name};`);
|
|
1053
|
+
lines.push(` unmount(): void;`);
|
|
1054
|
+
lines.push(`}`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Recurse into child nodes
|
|
1058
|
+
if (head === 'program' || head === 'block') {
|
|
1059
|
+
for (let i = 1; i < sexpr.length; i++) {
|
|
1060
|
+
if (Array.isArray(sexpr[i])) {
|
|
1061
|
+
emitComponentTypes(sexpr[i], lines, indent, indentLevel, componentVars);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
// Also check inside export wrappers
|
|
1066
|
+
if (head === 'export' && Array.isArray(sexpr[1]) && !compNode) {
|
|
1067
|
+
emitComponentTypes(sexpr[1], lines, indent, indentLevel, componentVars);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
690
1071
|
// ============================================================================
|
|
691
1072
|
// generateEnum — runtime JavaScript enum object (CodeGenerator method)
|
|
692
1073
|
// ============================================================================
|