rip-lang 3.16.0 → 3.16.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 +3 -4
- package/bin/rip +201 -18
- package/bin/rip-schema +175 -0
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +200 -19
- package/docs/RIP-DUCKDB.md +64 -1
- package/docs/RIP-INTRO.md +4 -4
- package/docs/RIP-LANG.md +36 -38
- package/docs/RIP-SCHEMA.md +1204 -364
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +4195 -966
- package/docs/dist/rip.min.js +1161 -284
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +7 -7
- package/docs/example/index.json.br +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/index.html +2 -1
- package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/vscode-rip-0.6.0.vsix +0 -0
- package/docs/index.html +1 -1
- package/docs/ui/bundle.json +55 -55
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/hljs-rip.js +1 -1
- package/docs/ui/index.html +1 -1
- package/package.json +15 -7
- package/rip-loader.js +59 -2
- package/src/AGENTS.md +43 -12
- package/src/browser.js +52 -11
- package/src/compiler.js +538 -80
- package/src/components.js +488 -48
- package/src/dts.js +80 -50
- package/src/grammar/README.md +29 -170
- package/src/grammar/grammar.rip +17 -12
- package/src/grammar/solar.rip +4 -17
- package/src/lexer.js +82 -32
- package/src/parser.js +229 -229
- package/src/schema/dts.js +328 -54
- package/src/schema/loader-server.js +2 -1
- package/src/schema/runtime-browser-stubs.js +20 -9
- package/src/schema/runtime-ddl.js +161 -44
- package/src/schema/runtime-migrate.js +681 -0
- package/src/schema/runtime-orm.js +698 -54
- package/src/schema/runtime-validate.js +808 -24
- package/src/schema/runtime.generated.js +2395 -135
- package/src/schema/schema.js +1054 -94
- package/src/typecheck.js +1610 -127
- package/src/types.js +90 -6
- package/src/grammar/lunar.rip +0 -2412
- /package/docs/demo/{components → routes}/_layout.rip +0 -0
- /package/docs/demo/{components → routes}/about.rip +0 -0
- /package/docs/demo/{components → routes}/card.rip +0 -0
- /package/docs/demo/{components → routes}/counter.rip +0 -0
- /package/docs/demo/{components → routes}/index.rip +0 -0
- /package/docs/demo/{components → routes}/todos.rip +0 -0
package/src/dts.js
CHANGED
|
@@ -36,11 +36,11 @@ export const INTRINSIC_TYPE_DECLS = [
|
|
|
36
36
|
"type __RipAttrKeys<T> = { [K in keyof T]-?: K extends 'style' | 'classList' | 'className' | 'nodeValue' | 'textContent' | 'innerHTML' | 'innerText' | 'outerHTML' | 'outerText' | 'scrollLeft' | 'scrollTop' ? never : K extends `on${string}` | `aria${string}Element` | `aria${string}Elements` ? never : T[K] extends (...args: any[]) => any ? never : (<V>() => V extends Pick<T, K> ? 1 : 2) extends (<V>() => V extends { -readonly [P in K]: T[P] } ? 1 : 2) ? K : never }[keyof T] & string;",
|
|
37
37
|
"type __RipEvents<K extends __RipTag> = { [E in keyof HTMLElementEventMap as `@${E}`]?: ((event: RipEvent<HTMLElementEventMap[E], __RipElementMap[K]>) => void) | null };",
|
|
38
38
|
'type RipEvent<E extends Event, T extends EventTarget> = E & { readonly target: T; readonly currentTarget: T };',
|
|
39
|
-
'type __RipClassValue = string | boolean | null | undefined | Record<string, boolean> | __RipClassValue[];',
|
|
39
|
+
'type __RipClassValue = string | boolean | null | undefined | Record<string, boolean | null | undefined> | __RipClassValue[];',
|
|
40
40
|
"type __RipProps<K extends __RipTag> = { [P in __RipAttrKeys<__RipElementMap[K]>]?: __RipElementMap[K][P] } & __RipEvents<K> & { ref?: string; class?: __RipClassValue | __RipClassValue[]; style?: string; [k: `data-${string}`]: any; [k: `aria-${string}`]: any };",
|
|
41
41
|
];
|
|
42
42
|
|
|
43
|
-
export const INTRINSIC_FN_DECL = 'declare function __ripEl<K extends __RipTag>(tag: K, props?: __RipProps<K>): void;';
|
|
43
|
+
export const INTRINSIC_FN_DECL = 'declare function __ripEl<K extends __RipTag>(tag: K, props?: __RipProps<K>): void;\ndeclare function __ripRoute<const T extends string>(s: T): T;';
|
|
44
44
|
|
|
45
45
|
export const ARIA_TYPE_DECLS = [
|
|
46
46
|
'type __RipAriaNavHandlers = { next?: () => void; prev?: () => void; first?: () => void; last?: () => void; select?: () => void; dismiss?: () => void; tab?: () => void; char?: () => void; };',
|
|
@@ -67,12 +67,23 @@ export const SIGNAL_FN = 'declare function __state<T>(value: T | Signal<T>): Sig
|
|
|
67
67
|
export const COMPUTED_INTERFACE = 'interface Computed<T> { readonly value: T; read(): T; lock(): Computed<T>; free(): Computed<T>; kill(): T; }';
|
|
68
68
|
export const COMPUTED_FN = 'declare function __computed<T>(fn: () => T): Computed<T>;';
|
|
69
69
|
export const EFFECT_FN = 'declare function __effect(fn: () => void | (() => void)): () => void;';
|
|
70
|
+
export const BATCH_FN = 'declare function __batch<T>(fn: () => T): T;';
|
|
71
|
+
|
|
72
|
+
// Names destructured from `globalThis.__rip` in the source. The DTS
|
|
73
|
+
// preamble and the post-compile `declare function` injection both need
|
|
74
|
+
// to skip auto-declaring these names — the explicit binding shadows the
|
|
75
|
+
// global and would otherwise trip TS2630.
|
|
76
|
+
export function ripDestructuredNames(source) {
|
|
77
|
+
if (typeof source !== 'string') return new Set();
|
|
78
|
+
const inside = (source.match(/\{\s*([^}]*?)\s*\}\s*=\s*globalThis\.__rip\b/) || [])[1] || '';
|
|
79
|
+
return new Set(inside.split(',').map(s => s.trim().split(/[:\s]/)[0]).filter(Boolean));
|
|
80
|
+
}
|
|
70
81
|
|
|
71
82
|
// ============================================================================
|
|
72
83
|
// emitTypes — generate .d.ts from annotated tokens + s-expression tree
|
|
73
84
|
// ============================================================================
|
|
74
85
|
|
|
75
|
-
export function emitTypes(tokens, sexpr = null, source = '') {
|
|
86
|
+
export function emitTypes(tokens, sexpr = null, source = '', schemaBehavior = null, schemaAnon = null) {
|
|
76
87
|
let lines = [];
|
|
77
88
|
let indentLevel = 0;
|
|
78
89
|
let indentStr = ' ';
|
|
@@ -82,7 +93,9 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
82
93
|
let classFields = new Set(); // Track emitted field names to avoid duplicates
|
|
83
94
|
let usesSignal = false;
|
|
84
95
|
let usesComputed = false;
|
|
96
|
+
let usesBatch = false;
|
|
85
97
|
let usesRipIntrinsicProps = false;
|
|
98
|
+
const explicitlyBound = ripDestructuredNames(source);
|
|
86
99
|
const sourceLines = typeof source === 'string' ? source.split('\n') : [];
|
|
87
100
|
|
|
88
101
|
// Pre-scan: detect reactive operators regardless of type annotations.
|
|
@@ -92,6 +105,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
92
105
|
const tag = tokens[i][0];
|
|
93
106
|
if (tag === 'REACTIVE_ASSIGN') usesSignal = true;
|
|
94
107
|
else if (tag === 'COMPUTED_ASSIGN') usesComputed = true;
|
|
108
|
+
else if (tag === 'IDENTIFIER' && tokens[i][1] === '__batch') usesBatch = true;
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
// Format { prop; prop } into multi-line block. Only applies when the
|
|
@@ -195,7 +209,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
195
209
|
let type = tokens[j].data?.type;
|
|
196
210
|
if (type) hasAnyType = true;
|
|
197
211
|
let hasDefault = tokens[j + 1]?.[0] === '=';
|
|
198
|
-
props.push({ kind: 'rename', propName, localName, type: type ?
|
|
212
|
+
props.push({ kind: 'rename', propName, localName, type: type ? tsType(type) : null, hasDefault });
|
|
199
213
|
j++;
|
|
200
214
|
if (hasDefault) j = skipDefault(tokens, j);
|
|
201
215
|
}
|
|
@@ -208,7 +222,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
208
222
|
let type = tokens[j].data?.type;
|
|
209
223
|
if (type) hasAnyType = true;
|
|
210
224
|
let hasDefault = tokens[j + 1]?.[0] === '=';
|
|
211
|
-
props.push({ kind: 'simple', propName: name, type: type ?
|
|
225
|
+
props.push({ kind: 'simple', propName: name, type: type ? tsType(type) : null, hasDefault });
|
|
212
226
|
j++;
|
|
213
227
|
if (hasDefault) j = skipDefault(tokens, j);
|
|
214
228
|
continue;
|
|
@@ -259,7 +273,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
259
273
|
let name = tokens[j][1];
|
|
260
274
|
let type = tokens[j].data?.type;
|
|
261
275
|
names.push(name);
|
|
262
|
-
elemTypes.push(type ?
|
|
276
|
+
elemTypes.push(type ? tsType(type) : null);
|
|
263
277
|
if (type) hasAnyType = true;
|
|
264
278
|
}
|
|
265
279
|
j++;
|
|
@@ -308,8 +322,8 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
308
322
|
let name = tokens[j][1];
|
|
309
323
|
let type = tokens[j].data?.type;
|
|
310
324
|
let paramName = subclassConstructor ? `_${name}` : name;
|
|
311
|
-
params.push(type ? `${paramName}: ${
|
|
312
|
-
if (type) fields.push({ name, type:
|
|
325
|
+
params.push(type ? `${paramName}: ${tsType(type)}` : paramName);
|
|
326
|
+
if (type) fields.push({ name, type: tsType(type) });
|
|
313
327
|
j++;
|
|
314
328
|
}
|
|
315
329
|
continue;
|
|
@@ -321,7 +335,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
321
335
|
if (tokens[j]?.[0] === 'IDENTIFIER') {
|
|
322
336
|
let name = tokens[j][1];
|
|
323
337
|
let type = tokens[j].data?.type;
|
|
324
|
-
params.push(type ? `...${name}: ${
|
|
338
|
+
params.push(type ? `...${name}: ${tsType(type)}` : `...${name}: any[]`);
|
|
325
339
|
j++;
|
|
326
340
|
}
|
|
327
341
|
continue;
|
|
@@ -364,9 +378,9 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
364
378
|
hasDefault = true;
|
|
365
379
|
}
|
|
366
380
|
|
|
367
|
-
let isOptional = hasDefault || tok.data?.
|
|
381
|
+
let isOptional = hasDefault || tok.data?.optional;
|
|
368
382
|
if (paramType) {
|
|
369
|
-
params.push(`${paramName}${isOptional ? '?' : ''}: ${
|
|
383
|
+
params.push(`${paramName}${isOptional ? '?' : ''}: ${tsType(paramType)}`);
|
|
370
384
|
} else {
|
|
371
385
|
params.push(paramName);
|
|
372
386
|
}
|
|
@@ -486,7 +500,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
486
500
|
let nameToken = ot[1]; // DEF is [0], name is [1]
|
|
487
501
|
let { params: paramList } = collectParams(ot, 2);
|
|
488
502
|
let returnType = nameToken.data?.returnType;
|
|
489
|
-
let ret = returnType ? `: ${
|
|
503
|
+
let ret = returnType ? `: ${tsType(returnType)}` : '';
|
|
490
504
|
let declare = inClass ? '' : (exp ? '' : 'declare ');
|
|
491
505
|
let typeParams = data.typeParams || '';
|
|
492
506
|
if (inClass) {
|
|
@@ -498,7 +512,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
498
512
|
let ext = data.extends ? ` extends ${data.extends}` : '';
|
|
499
513
|
emitBlock(`${exp}interface ${data.name}${params}${ext} `, data.typeText || '{}', '');
|
|
500
514
|
} else {
|
|
501
|
-
let typeText =
|
|
515
|
+
let typeText = tsType(data.typeText || '');
|
|
502
516
|
emitBlock(`${exp}type ${data.name}${params} = `, typeText, ';');
|
|
503
517
|
}
|
|
504
518
|
continue;
|
|
@@ -594,7 +608,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
594
608
|
if (!nameToken) continue;
|
|
595
609
|
let fnName = nameToken[1];
|
|
596
610
|
let returnType = nameToken.data?.returnType;
|
|
597
|
-
if (!returnType && nameToken.data?.
|
|
611
|
+
if (!returnType && nameToken.data?.bang === true) returnType = 'void';
|
|
598
612
|
let typeParams = nameToken.data?.typeParams || '';
|
|
599
613
|
|
|
600
614
|
let { params, endIdx } = collectParams(tokens, i + 2);
|
|
@@ -603,7 +617,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
603
617
|
if (returnType || params.some(p => p.includes(':'))) {
|
|
604
618
|
let exp = exported ? 'export ' : '';
|
|
605
619
|
let declare = inClass ? '' : (exported ? '' : 'declare ');
|
|
606
|
-
let ret = returnType ? `: ${
|
|
620
|
+
let ret = returnType ? `: ${tsType(returnType)}` : '';
|
|
607
621
|
let paramStr = params.join(', ');
|
|
608
622
|
if (inClass) {
|
|
609
623
|
lines.push(`${indent()}${fnName}${typeParams}(${paramStr})${ret};`);
|
|
@@ -664,7 +678,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
664
678
|
}
|
|
665
679
|
|
|
666
680
|
if (returnType || params.some(p => p.includes(':'))) {
|
|
667
|
-
let ret = returnType ? `: ${
|
|
681
|
+
let ret = returnType ? `: ${tsType(returnType)}` : '';
|
|
668
682
|
let paramStr = params.join(', ');
|
|
669
683
|
// Emit field declarations for constructor @param:: type shorthand
|
|
670
684
|
if (methodName === 'constructor' && fields.length) {
|
|
@@ -717,7 +731,12 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
717
731
|
//
|
|
718
732
|
// The function-form is wrong inside bodies because it would create
|
|
719
733
|
// a phantom global that shadows / collides with the actual local.
|
|
720
|
-
|
|
734
|
+
//
|
|
735
|
+
// If the IDENTIFIER carries an explicit `name:: T = arrow` annotation,
|
|
736
|
+
// skip this path and let the typed-variable-assignment path below
|
|
737
|
+
// emit the typed form (otherwise we'd lose the user's annotation in
|
|
738
|
+
// favor of inferred `any` rest-param emission).
|
|
739
|
+
if (tag === 'IDENTIFIER' && !inClass && !t.data?.type &&
|
|
721
740
|
tokens[i + 1]?.[0] === '=' &&
|
|
722
741
|
(tokens[i + 2]?.[0] === 'PARAM_START' || tokens[i + 2]?.[0] === '(')) {
|
|
723
742
|
let fnName = t[1];
|
|
@@ -741,7 +760,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
741
760
|
let paramStr = params.join(', ');
|
|
742
761
|
if (bodyDepth === 0) {
|
|
743
762
|
let declare = exported ? '' : 'declare ';
|
|
744
|
-
let ret = returnType ? `: ${
|
|
763
|
+
let ret = returnType ? `: ${tsType(returnType)}` : '';
|
|
745
764
|
lines.push(`${indent()}${exp}${declare}function ${fnName}(${paramStr})${ret};`);
|
|
746
765
|
} else {
|
|
747
766
|
// `any` rather than `unknown` for the inferred-return case.
|
|
@@ -751,7 +770,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
751
770
|
// user didn't provide one. `unknown` would force callers to
|
|
752
771
|
// narrow before using the result, creating new false
|
|
753
772
|
// positives at every call site.
|
|
754
|
-
let ret = returnType ?
|
|
773
|
+
let ret = returnType ? tsType(returnType) : 'any';
|
|
755
774
|
lines.push(`${indent()}let ${fnName}: (${paramStr}) => ${ret};`);
|
|
756
775
|
}
|
|
757
776
|
continue;
|
|
@@ -761,7 +780,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
761
780
|
// Variable assignments with type annotations
|
|
762
781
|
if (tag === 'IDENTIFIER' && t.data?.type) {
|
|
763
782
|
let varName = t[1];
|
|
764
|
-
let type =
|
|
783
|
+
let type = tsType(t.data.type);
|
|
765
784
|
let next = tokens[i + 1];
|
|
766
785
|
|
|
767
786
|
if (next) {
|
|
@@ -778,6 +797,12 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
778
797
|
lines.push(`${indent()}${exp}${declare}const ${varName}: Computed<${type}>;`);
|
|
779
798
|
} else if (next[0] === 'EFFECT') {
|
|
780
799
|
lines.push(`${indent()}${exp}${declare}const ${varName}: () => void;`);
|
|
800
|
+
} else if (next[0] === 'GATE') {
|
|
801
|
+
// Render-ready gate (<~) — only valid inside component
|
|
802
|
+
// bodies, where the component stub (not this hoist) types the
|
|
803
|
+
// binding. Explicit no-op so an annotated gate never falls
|
|
804
|
+
// through to the `=` arrow-scan below.
|
|
805
|
+
continue;
|
|
781
806
|
} else if (next[0] === '=') {
|
|
782
807
|
// Check if RHS is an arrow function with return type
|
|
783
808
|
let arrowIdx = i + 2;
|
|
@@ -795,14 +820,23 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
795
820
|
if (arrowToken && (arrowToken[0] === '->' || arrowToken[0] === '=>') &&
|
|
796
821
|
arrowToken.data?.returnType) {
|
|
797
822
|
// Typed arrow function assignment
|
|
798
|
-
let returnType =
|
|
823
|
+
let returnType = tsType(arrowToken.data.returnType);
|
|
799
824
|
let { params } = collectParams(tokens, i + 2);
|
|
800
825
|
let paramStr = params.join(', ');
|
|
801
826
|
lines.push(`${indent()}${exp}${declare}function ${varName}(${paramStr}): ${returnType};`);
|
|
802
827
|
} else if (inClass) {
|
|
803
828
|
lines.push(`${indent()}${varName}: ${type};`);
|
|
804
829
|
classFields.add(varName);
|
|
805
|
-
} else {
|
|
830
|
+
} else if (bodyDepth === 0) {
|
|
831
|
+
// Module-scope typed binding: typecheck.js's inline-let pass
|
|
832
|
+
// merges this header line into the body's hoist by NAME. That
|
|
833
|
+
// name-based merge is only sound at module scope — for a
|
|
834
|
+
// function-local (`bodyDepth > 0`) the same name can exist
|
|
835
|
+
// untyped in sibling functions, and the merge would stamp this
|
|
836
|
+
// annotation onto every one of them. Function-locals don't
|
|
837
|
+
// need the header line anyway: the compiler's typed-local
|
|
838
|
+
// hoist (collectTypedLocals) annotates the owning function's
|
|
839
|
+
// own `let` in the body, scope-correctly.
|
|
806
840
|
lines.push(`${indent()}${exp}let ${varName}: ${type};`);
|
|
807
841
|
}
|
|
808
842
|
} else if (inClass) {
|
|
@@ -833,7 +867,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
833
867
|
// Schema declarations — strip any prior auto-emitted `declare let Foo`
|
|
834
868
|
// for the same bindings (they are re-emitted as typed Schema<T>).
|
|
835
869
|
let schemaLines = [];
|
|
836
|
-
hasSchemaDecls = emitSchemaTypes(sexpr, schemaLines);
|
|
870
|
+
hasSchemaDecls = emitSchemaTypes(sexpr, schemaLines, schemaBehavior, schemaAnon);
|
|
837
871
|
if (hasSchemaDecls) {
|
|
838
872
|
let bindings = new Set();
|
|
839
873
|
for (let line of schemaLines) {
|
|
@@ -860,15 +894,18 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
860
894
|
}
|
|
861
895
|
if (usesSignal) {
|
|
862
896
|
preamble.push(SIGNAL_INTERFACE);
|
|
863
|
-
preamble.push(SIGNAL_FN);
|
|
897
|
+
if (!explicitlyBound.has('__state')) preamble.push(SIGNAL_FN);
|
|
864
898
|
}
|
|
865
899
|
if (usesComputed) {
|
|
866
900
|
preamble.push(COMPUTED_INTERFACE);
|
|
867
|
-
preamble.push(COMPUTED_FN);
|
|
901
|
+
if (!explicitlyBound.has('__computed')) preamble.push(COMPUTED_FN);
|
|
868
902
|
}
|
|
869
|
-
if (usesSignal || usesComputed) {
|
|
903
|
+
if ((usesSignal || usesComputed) && !explicitlyBound.has('__effect')) {
|
|
870
904
|
preamble.push(EFFECT_FN);
|
|
871
905
|
}
|
|
906
|
+
if ((usesSignal || usesComputed || usesBatch) && !explicitlyBound.has('__batch')) {
|
|
907
|
+
preamble.push(BATCH_FN);
|
|
908
|
+
}
|
|
872
909
|
if (hasSchemaDecls) {
|
|
873
910
|
preamble.push(...SCHEMA_INTRINSIC_DECLS);
|
|
874
911
|
}
|
|
@@ -880,25 +917,16 @@ export function emitTypes(tokens, sexpr = null, source = '') {
|
|
|
880
917
|
}
|
|
881
918
|
|
|
882
919
|
// ============================================================================
|
|
883
|
-
//
|
|
920
|
+
// Convert a Rip type expression to its TypeScript form.
|
|
884
921
|
// ============================================================================
|
|
922
|
+
//
|
|
923
|
+
// Today this only strips the `::` annotation sigil to `:`. Kept as a
|
|
924
|
+
// dedicated function so every call site routes through one place if the
|
|
925
|
+
// conversion ever needs to grow.
|
|
885
926
|
|
|
886
|
-
function
|
|
927
|
+
function tsType(typeStr) {
|
|
887
928
|
if (!typeStr) return typeStr;
|
|
888
|
-
|
|
889
|
-
// Convert :: to : (annotation sigil to type separator)
|
|
890
|
-
typeStr = typeStr.replace(/::/g, ':');
|
|
891
|
-
|
|
892
|
-
// T?? → T | null | undefined
|
|
893
|
-
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?\?/g, '$1 | null | undefined');
|
|
894
|
-
|
|
895
|
-
// T? → T | undefined (but not ?. or ?: which are different)
|
|
896
|
-
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?(?![.:])/g, '$1 | undefined');
|
|
897
|
-
|
|
898
|
-
// T! → NonNullable<T>
|
|
899
|
-
typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\!/g, 'NonNullable<$1>');
|
|
900
|
-
|
|
901
|
-
return typeStr;
|
|
929
|
+
return typeStr.replace(/::/g, ':');
|
|
902
930
|
}
|
|
903
931
|
|
|
904
932
|
// ============================================================================
|
|
@@ -992,18 +1020,18 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
|
|
|
992
1020
|
if (!Array.isArray(member)) continue;
|
|
993
1021
|
let mHead = member[0]?.valueOf?.() ?? member[0];
|
|
994
1022
|
|
|
995
|
-
let target, propName, isProp, type,
|
|
1023
|
+
let target, propName, isProp, type, optional;
|
|
996
1024
|
|
|
997
1025
|
if (mHead === 'state' || mHead === 'readonly' || mHead === 'computed') {
|
|
998
1026
|
target = member[1];
|
|
999
1027
|
isProp = Array.isArray(target) && (target[0]?.valueOf?.() ?? target[0]) === '.' && (target[1]?.valueOf?.() ?? target[1]) === 'this';
|
|
1000
1028
|
propName = isProp ? (target[2]?.valueOf?.() ?? target[2]) : (target?.valueOf?.() ?? target);
|
|
1001
1029
|
type = isProp ? target[2]?.type : target?.type;
|
|
1002
|
-
|
|
1030
|
+
optional = isProp ? !!target[2]?.optional : !!target?.optional;
|
|
1003
1031
|
if (!isProp) {
|
|
1004
1032
|
componentVars.add(propName);
|
|
1005
1033
|
let wrapper = (mHead === 'computed') ? 'Computed' : 'Signal';
|
|
1006
|
-
let typeStr = type ?
|
|
1034
|
+
let typeStr = type ? tsType(type) : (inferLiteralType(member[2]) || 'any');
|
|
1007
1035
|
bodyMembers.push(` ${propName}: ${wrapper}<${typeStr}>;`);
|
|
1008
1036
|
continue;
|
|
1009
1037
|
}
|
|
@@ -1011,7 +1039,7 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
|
|
|
1011
1039
|
isProp = (member[1]?.valueOf?.() ?? member[1]) === 'this';
|
|
1012
1040
|
propName = isProp ? (member[2]?.valueOf?.() ?? member[2]) : null;
|
|
1013
1041
|
type = isProp ? member[2]?.type : null;
|
|
1014
|
-
|
|
1042
|
+
optional = isProp ? !!member[2]?.optional : false;
|
|
1015
1043
|
if (!isProp && propName) componentVars.add(propName);
|
|
1016
1044
|
} else if (mHead === 'object') {
|
|
1017
1045
|
// Method definitions: (object (: methodName (-> (params...) (block ...))))
|
|
@@ -1041,7 +1069,7 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
|
|
|
1041
1069
|
for (let p of params) {
|
|
1042
1070
|
let { inner, hasDefault } = unwrapDefault(p);
|
|
1043
1071
|
let pName = inner?.valueOf?.() ?? inner;
|
|
1044
|
-
let pType = inner?.type ?
|
|
1072
|
+
let pType = inner?.type ? tsType(inner.type) : 'any';
|
|
1045
1073
|
// Defaulted params are optional in the type signature so callers
|
|
1046
1074
|
// may omit them.
|
|
1047
1075
|
let opt = hasDefault ? '?' : '';
|
|
@@ -1060,9 +1088,11 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
|
|
|
1060
1088
|
|
|
1061
1089
|
if (!isProp || !propName) continue;
|
|
1062
1090
|
|
|
1063
|
-
let typeStr = type ?
|
|
1064
|
-
|
|
1065
|
-
|
|
1091
|
+
let typeStr = type ? tsType(type) : 'any';
|
|
1092
|
+
// `?` on the prop name is the sole optionality marker;
|
|
1093
|
+
// `:=` defaults do not imply optionality.
|
|
1094
|
+
let opt = optional ? '?' : '';
|
|
1095
|
+
if (!optional) hasRequired = true;
|
|
1066
1096
|
publicProps.push(` ${propName}${opt}: ${typeStr};`);
|
|
1067
1097
|
if (mHead === 'state') {
|
|
1068
1098
|
publicProps.push(` __bind_${propName}__?: Signal<${typeStr}>;`);
|
package/src/grammar/README.md
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
# Solar
|
|
1
|
+
# Solar — Rip's Parser Generator
|
|
2
2
|
|
|
3
|
-
One grammar.
|
|
3
|
+
One grammar. One parser. Mathematically derived.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
grammar.rip ──→ Solar ──→ parser.js
|
|
7
|
-
└→ Lunar ──→ parser-rd.js (predictive recursive descent, 110KB)
|
|
6
|
+
grammar.rip ──→ Solar ──→ parser.js (SLR(1) table-driven)
|
|
8
7
|
```
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
independent implementations derived from the same grammar specification.
|
|
13
|
-
|
|
14
|
-
**Test parity:** 1,162 / 1,182 tests passing (98.3%) — 20 files at 100%.
|
|
9
|
+
The parser accepts the token stream from the lexer and produces s-expression
|
|
10
|
+
ASTs — simple arrays like `["=", "x", 42]` — that the code emitter consumes.
|
|
15
11
|
|
|
16
12
|
---
|
|
17
13
|
|
|
18
14
|
## Files
|
|
19
15
|
|
|
20
|
-
| File |
|
|
21
|
-
|
|
22
|
-
| `grammar.rip` |
|
|
23
|
-
| `solar.rip` |
|
|
24
|
-
|
|
16
|
+
| File | Purpose |
|
|
17
|
+
|------|---------|
|
|
18
|
+
| `grammar.rip` | Grammar specification — defines all syntax rules |
|
|
19
|
+
| `solar.rip` | SLR(1) parser generator — produces the table-driven parser |
|
|
20
|
+
|
|
21
|
+
`src/parser.js` is the generated output. Never edit it by hand — regenerate
|
|
22
|
+
with `bun run parser` after any grammar change.
|
|
25
23
|
|
|
26
24
|
---
|
|
27
25
|
|
|
@@ -51,6 +49,13 @@ Operation: [
|
|
|
51
49
|
]
|
|
52
50
|
```
|
|
53
51
|
|
|
52
|
+
Action format:
|
|
53
|
+
|
|
54
|
+
- Numbers (`1`, `2`, `...3`) reference matched symbols by position
|
|
55
|
+
- `...N` spreads the array at position N
|
|
56
|
+
- String literals become s-expression nodes: `'["if", 2, 3]'`
|
|
57
|
+
- Default action (no action given) returns position 1
|
|
58
|
+
|
|
54
59
|
---
|
|
55
60
|
|
|
56
61
|
## Solar (`solar.rip`) — SLR(1) Table Parser Generator
|
|
@@ -66,169 +71,23 @@ Grammar → Process Rules → Build LR Automaton → Compute FIRST/FOLLOW
|
|
|
66
71
|
|
|
67
72
|
A table-driven parser where every parsing decision is a lookup:
|
|
68
73
|
`parseTable[state][symbol]` → shift, reduce, or accept. The parse table
|
|
69
|
-
encodes
|
|
74
|
+
encodes all states with transitions delta-compressed for minimal size.
|
|
70
75
|
|
|
71
76
|
### How to use
|
|
72
77
|
|
|
73
78
|
```bash
|
|
79
|
+
bun run parser # Regenerate src/parser.js
|
|
74
80
|
bun src/grammar/solar.rip grammar.rip # → parser.js (SLR table)
|
|
75
|
-
bun src/grammar/solar.rip -r grammar.rip # → parser-rd.js (PRD via Lunar)
|
|
76
81
|
bun src/grammar/solar.rip --info grammar.rip # Show grammar statistics
|
|
82
|
+
bun src/grammar/solar.rip --info --conflicts grammar.rip # Conflict details
|
|
77
83
|
bun src/grammar/solar.rip --sexpr grammar.rip # Show grammar as s-expression
|
|
84
|
+
bun src/grammar/solar.rip -o out.js grammar.rip # Custom output path
|
|
78
85
|
```
|
|
79
86
|
|
|
80
|
-
###
|
|
81
|
-
|
|
82
|
-
Solar imports Lunar and installs it with one line:
|
|
83
|
-
|
|
84
|
-
```coffee
|
|
85
|
-
import { install as installLunar } from './lunar.rip'
|
|
86
|
-
# ... (Generator class definition) ...
|
|
87
|
-
installLunar Generator
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
This adds `generateRD()` to the Generator prototype. When `-r` is passed,
|
|
91
|
-
Solar calls `generator.generateRD()` instead of `generator.generate()`.
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## Lunar (`lunar.rip`) — Predictive Recursive Descent Generator
|
|
96
|
-
|
|
97
|
-
Lunar analyzes the same grammar that Solar processes and generates a
|
|
98
|
-
hand-rolled-looking recursive descent parser with Pratt expression parsing.
|
|
99
|
-
|
|
100
|
-
### Architecture
|
|
101
|
-
|
|
102
|
-
The generated parser has five layers:
|
|
103
|
-
|
|
104
|
-
1. **Token management** — `advance()`, `expect()`, `match()`, `loc()`, `withLoc()`
|
|
105
|
-
2. **Speculation** — `mark()`, `reset()`, `speculate()` for backtracking
|
|
106
|
-
3. **Pratt expression parser** — `parseExpression(minBP)` with binding powers
|
|
107
|
-
4. **Nonterminal functions** — one `parseX()` function per grammar nonterminal
|
|
108
|
-
5. **Parser shell** — same API as the table parser (`parser.parse()`, exports)
|
|
109
|
-
|
|
110
|
-
### How it works
|
|
111
|
-
|
|
112
|
-
Lunar derives everything from the grammar — no hardcoded token or nonterminal
|
|
113
|
-
names in the core generators:
|
|
114
|
-
|
|
115
|
-
**Expression analysis** (`_analyzeExpressionRules`) — walks the grammar to detect:
|
|
116
|
-
- Which nonterminal is the "expression" (contains Operation as an alternative)
|
|
117
|
-
- Which is the "operation" (has the most `NT OP NT` binary rules with precedence)
|
|
118
|
-
- Which is the "value" (pure choice nonterminal with the most atom alternatives)
|
|
119
|
-
- Which is "code" (FIRST set contains `->` or `=>`)
|
|
120
|
-
- Assignment operators (nonterminals where all rules are `LHS TOKEN RHS`)
|
|
121
|
-
- Prefix starters (keyword tokens that begin expression alternatives)
|
|
122
|
-
- Postfix chains (left-recursive property/index/call rules through Value chain)
|
|
123
|
-
- Atom types (terminals reachable through the Value nonterminal chain)
|
|
124
|
-
- Expression-handled tokens (for skipping redundant choice alternatives)
|
|
125
|
-
|
|
126
|
-
**Pratt parser generation** (`_generateRDExpression`) — builds the while loop:
|
|
127
|
-
- Prefix starters dispatch to keyword-led parsers (IF→parseIf, FOR→parseFor, etc.)
|
|
128
|
-
- Assignment operators checked at binding power 0
|
|
129
|
-
- Postfix operators from Operation rules (with INDENT/TERMINATOR variants)
|
|
130
|
-
- Postfix chains from property/index/call rules (resolved to FIRST sets)
|
|
131
|
-
- Infix binary operators with correct associativity and control-flow merging
|
|
132
|
-
- Ternary operators
|
|
133
|
-
- Postfix if/unless/while/until and comprehensions
|
|
134
|
-
- Statement tokens (RETURN, STATEMENT) enter the Pratt loop for postfix patterns
|
|
135
|
-
|
|
136
|
-
**Nonterminal classification** (`_classifyNonterminal`) — detects patterns:
|
|
137
|
-
|
|
138
|
-
| Pattern | Detection | Example |
|
|
139
|
-
|---------|-----------|---------|
|
|
140
|
-
| `root` | Grammar start symbol | Root |
|
|
141
|
-
| `body-list` | Left-recursive with TERMINATOR | Body, ComponentBody |
|
|
142
|
-
| `comma-list` | Left-recursive with `,` | ArgList, ParamList |
|
|
143
|
-
| `concat-list` | Left-recursive, no separator | Interpolations, Cases |
|
|
144
|
-
| `left-rec-loop` | Self-referential with terminal continuation | IfBlock |
|
|
145
|
-
| `expression` | Contains the operation nonterminal | Expression |
|
|
146
|
-
| `operation` | Has binary operator rules | Operation |
|
|
147
|
-
| `token` | Single rule, single terminal | Identifier, Property |
|
|
148
|
-
| `keyword` | All rules start with unique terminals | Return, Def, Enum |
|
|
149
|
-
| `choice` | All rules are single-nonterminal passthroughs | Value, Line, Statement |
|
|
150
|
-
| `sequence` | Everything else | Assign, Catch, Block |
|
|
151
|
-
|
|
152
|
-
**Shared prefix disambiguation** (`_generateRDSharedPrefix`) — handles rules
|
|
153
|
-
that share a common beginning:
|
|
154
|
-
- Optional chain detection with rule-length-based action dispatch
|
|
155
|
-
- Same-token grouping with deeper disambiguation
|
|
156
|
-
- Terminal and nonterminal suffix separation with FIRST set grouping
|
|
157
|
-
- Empty-rule-as-default when nonterminal suffixes have FIRST checks
|
|
158
|
-
|
|
159
|
-
**Speculation** — `mark()`/`reset()`/`speculate()` for grammar ambiguities:
|
|
160
|
-
- Range vs Array: `[1..10]` vs `[1, 2, 3]` — try Range first
|
|
161
|
-
- Slice vs Expression in INDEX_START: `arr[1..3]` vs `arr[0]`
|
|
162
|
-
- Range vs destructuring in For: `for [1..5]` vs `for [a, b] as iter`
|
|
163
|
-
- Terminal/nonterminal overlap: `...` as Splat vs expansion marker
|
|
164
|
-
- Left-rec-loop lookahead: `ELSE IF` vs `ELSE Block` in IfBlock
|
|
165
|
-
|
|
166
|
-
### Three specialized generators
|
|
167
|
-
|
|
168
|
-
Three nonterminals (out of 93) have patterns that require multi-token lookahead
|
|
169
|
-
and can't be handled by the generic generators:
|
|
170
|
-
|
|
171
|
-
- **For** (34 rules) — FORIN/FOROF/FORAS variants with optional WHEN/BY in both orders
|
|
172
|
-
- **Object** (5 rules) — comprehension vs regular object determined after parsing key:value
|
|
173
|
-
- **AssignObj** (6 rules) — key:value vs key=default vs shorthand vs rest after parsing key
|
|
174
|
-
|
|
175
|
-
90 of 93 nonterminals (96.8%) are generated purely from grammar analysis.
|
|
176
|
-
|
|
177
|
-
### Remaining 20 test failures (98.3% → 100%)
|
|
178
|
-
|
|
179
|
-
The remaining failures cluster into a few fixable categories:
|
|
180
|
-
|
|
181
|
-
| Category | Tests | Root Cause |
|
|
182
|
-
|----------|-------|------------|
|
|
183
|
-
| **Array elisions** | 5 | `[,1]`, `[1,,3]` — ArgElisionList/Elision not parsed |
|
|
184
|
-
| **Trailing comma** | 1 | `[1,2,]` — OptElisions at end of array |
|
|
185
|
-
| **Export/Import edge** | 3 | `export { x }` without FROM, `export x ~= ...`, `import x, * as m` — deeper shared-prefix disambiguation generates duplicate conditions |
|
|
186
|
-
| **Semicolons context** | 3 | `def foo(); 42` — Def/async without block body (CALL_END before Block) |
|
|
187
|
-
| **Class patterns** | 2 | Class expression without name, `@bar:` static in class body |
|
|
188
|
-
| **Postfix ternary** | 1 | `a = x if true else 0` — assignment context for postfix ternary |
|
|
189
|
-
| **Other edge cases** | 5 | `invalid extends`, `array destructuring skip`, type alias, typed runtime |
|
|
190
|
-
|
|
191
|
-
**Key fix needed for Export/Import:** The shared-prefix handler generates duplicate
|
|
192
|
-
`else if` branches with identical conditions when two rules share a common prefix
|
|
193
|
-
through nonterminals but diverge at a terminal after parsing (e.g., `} FROM` vs `}`).
|
|
194
|
-
The inner terminal group disambiguator needs to check terminal continuation instead
|
|
195
|
-
of generating separate branches.
|
|
196
|
-
|
|
197
|
-
**Key fix needed for elisions:** The Array parser routes `[` to Range speculation
|
|
198
|
-
then Array. Array uses ArgElisionList which calls Arg → Expression. But leading
|
|
199
|
-
commas `[,1]` and sparse `[1,,3]` need the Elision nonterminal which the generic
|
|
200
|
-
comma-list handler doesn't generate.
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## Comparison
|
|
205
|
-
|
|
206
|
-
| Aspect | Solar (SLR) | Lunar (PRD) |
|
|
207
|
-
|--------|-------------|-------------|
|
|
208
|
-
| Strategy | Bottom-up table lookup | Top-down predictive |
|
|
209
|
-
| Output size | 215KB (encoded tables) | 110KB (readable code) |
|
|
210
|
-
| Startup | Decode table on load | Zero initialization |
|
|
211
|
-
| Debugging | "State 437" errors | Named function call stacks |
|
|
212
|
-
| Correctness | Mathematically derived | Grammar-derived + 3 specializations |
|
|
213
|
-
| Test parity | 1,235/1,235 (100%) | 1,162/1,182 (98.3%) |
|
|
214
|
-
| Speed | O(n) tight loop | O(n) function calls |
|
|
215
|
-
| Expressions | Shift/reduce with precedence | Pratt with binding powers |
|
|
216
|
-
|
|
217
|
-
Both produce identical s-expressions for 98.3% of the test suite. Having two
|
|
218
|
-
independent implementations from the same grammar provides cross-validation.
|
|
219
|
-
|
|
220
|
-
---
|
|
221
|
-
|
|
222
|
-
## The Innovation
|
|
223
|
-
|
|
224
|
-
Most parser generators commit to one strategy: Yacc produces LALR tables,
|
|
225
|
-
ANTLR produces LL recursive descent, PEG.js produces PEG parsers. Solar and
|
|
226
|
-
Lunar generate **both** from a single grammar specification.
|
|
227
|
-
|
|
228
|
-
The key insight: the FIRST/FOLLOW sets and operator precedence table that
|
|
229
|
-
Solar computes for SLR(1) contain all the information a Pratt-based recursive
|
|
230
|
-
descent parser needs. The data is the same — it's just read differently.
|
|
231
|
-
Solar reads it as table entries. Lunar reads it as dispatch conditions and
|
|
232
|
-
binding powers.
|
|
87
|
+
### Workflow for grammar changes
|
|
233
88
|
|
|
234
|
-
|
|
89
|
+
1. Edit `src/grammar/grammar.rip`
|
|
90
|
+
2. Run `bun run parser`
|
|
91
|
+
3. Verify the new parse shape with `echo 'code' | ./bin/rip -s`
|
|
92
|
+
4. Update codegen in `src/compiler.js` if the node shape changed
|
|
93
|
+
5. Run `bun run test`
|
package/src/grammar/grammar.rip
CHANGED
|
@@ -96,6 +96,7 @@ grammar =
|
|
|
96
96
|
o 'ComputedAssign'
|
|
97
97
|
o 'ReadonlyAssign'
|
|
98
98
|
o 'Effect'
|
|
99
|
+
o 'Gate'
|
|
99
100
|
o 'If'
|
|
100
101
|
o 'Try'
|
|
101
102
|
o 'While'
|
|
@@ -233,6 +234,13 @@ grammar =
|
|
|
233
234
|
o 'Assignable READONLY_ASSIGN INDENT Expression OUTDENT', '["readonly", 1, 4]'
|
|
234
235
|
]
|
|
235
236
|
|
|
237
|
+
# Render-ready gate (<~) — load-before-render binding; component bodies only.
|
|
238
|
+
# One form on purpose: the RHS must be a one-line literal stash path, so the
|
|
239
|
+
# compiler can hoist it into the component's static gate-set.
|
|
240
|
+
Gate: [
|
|
241
|
+
o 'Assignable GATE Expression', '["gate", 1, 3]'
|
|
242
|
+
]
|
|
243
|
+
|
|
236
244
|
# Reactive effect (~>) — side effects that run when dependencies change
|
|
237
245
|
Effect: [
|
|
238
246
|
o 'Assignable EFFECT Expression' , '["effect", 1, 3]'
|
|
@@ -592,14 +600,15 @@ grammar =
|
|
|
592
600
|
o 'INDENT Body OUTDENT', '["block", ...2]'
|
|
593
601
|
]
|
|
594
602
|
|
|
595
|
-
# Parenthetical mark-up:
|
|
596
|
-
#
|
|
597
|
-
#
|
|
598
|
-
#
|
|
599
|
-
#
|
|
603
|
+
# Parenthetical mark-up: tag a single parenthesized expression with
|
|
604
|
+
# `.parenthesized = true`. Consumers that respect explicit grouping
|
|
605
|
+
# consult it: the codegen ternary-hoist (so `(x = "a") if cond else "b"`
|
|
606
|
+
# stays a conditional assign) and comparison chaining (so
|
|
607
|
+
# `(a == b) == c` compares the boolean instead of chaining). The marker
|
|
608
|
+
# is harmless for any other consumer.
|
|
600
609
|
Parenthetical: [
|
|
601
|
-
o '( Body )' , '$2.length === 1 ? (Array.isArray($2[0]) &&
|
|
602
|
-
o '( INDENT Body OUTDENT )', '$3.length === 1 ? (Array.isArray($3[0]) &&
|
|
610
|
+
o '( Body )' , '$2.length === 1 ? (Array.isArray($2[0]) && ($2[0].parenthesized = true), $2[0]) : ["block", ...$2]'
|
|
611
|
+
o '( INDENT Body OUTDENT )', '$3.length === 1 ? (Array.isArray($3[0]) && ($3[0].parenthesized = true), $3[0]) : ["block", ...$3]'
|
|
603
612
|
]
|
|
604
613
|
|
|
605
614
|
OptComma: [
|
|
@@ -1004,9 +1013,6 @@ grammar =
|
|
|
1004
1013
|
o 'Expression || Expression' , '["||", 1, 3]'
|
|
1005
1014
|
o 'Expression ?? Expression' , '["??", 1, 3]'
|
|
1006
1015
|
|
|
1007
|
-
# Pipe
|
|
1008
|
-
o 'Expression PIPE Expression' , '["|>", 1, 3]'
|
|
1009
|
-
|
|
1010
1016
|
# Relational
|
|
1011
1017
|
o 'Expression RELATION Expression', '[2, 1, 3]' # in, of, instanceof
|
|
1012
1018
|
|
|
@@ -1046,8 +1052,7 @@ operators = """
|
|
|
1046
1052
|
left ^
|
|
1047
1053
|
left |
|
|
1048
1054
|
left &&
|
|
1049
|
-
left ||
|
|
1050
|
-
left PIPE
|
|
1055
|
+
left || ??
|
|
1051
1056
|
right TERNARY
|
|
1052
1057
|
nonassoc INDENT OUTDENT
|
|
1053
1058
|
right YIELD
|