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.
Files changed (57) hide show
  1. package/README.md +3 -4
  2. package/bin/rip +201 -18
  3. package/bin/rip-schema +175 -0
  4. package/docs/AGENTS.md +1 -1
  5. package/docs/RIP-APP.md +200 -19
  6. package/docs/RIP-DUCKDB.md +64 -1
  7. package/docs/RIP-INTRO.md +4 -4
  8. package/docs/RIP-LANG.md +36 -38
  9. package/docs/RIP-SCHEMA.md +1204 -364
  10. package/docs/RIP-TYPES.md +74 -103
  11. package/docs/demo/README.md +4 -3
  12. package/docs/dist/rip.js +4195 -966
  13. package/docs/dist/rip.min.js +1161 -284
  14. package/docs/dist/rip.min.js.br +0 -0
  15. package/docs/example/index.json +7 -7
  16. package/docs/example/index.json.br +0 -0
  17. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  18. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  19. package/docs/extensions/vscode/rip/index.html +2 -1
  20. package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
  21. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  22. package/docs/extensions/vscode/rip/vscode-rip-0.6.0.vsix +0 -0
  23. package/docs/index.html +1 -1
  24. package/docs/ui/bundle.json +55 -55
  25. package/docs/ui/bundle.json.br +0 -0
  26. package/docs/ui/hljs-rip.js +1 -1
  27. package/docs/ui/index.html +1 -1
  28. package/package.json +15 -7
  29. package/rip-loader.js +59 -2
  30. package/src/AGENTS.md +43 -12
  31. package/src/browser.js +52 -11
  32. package/src/compiler.js +538 -80
  33. package/src/components.js +488 -48
  34. package/src/dts.js +80 -50
  35. package/src/grammar/README.md +29 -170
  36. package/src/grammar/grammar.rip +17 -12
  37. package/src/grammar/solar.rip +4 -17
  38. package/src/lexer.js +82 -32
  39. package/src/parser.js +229 -229
  40. package/src/schema/dts.js +328 -54
  41. package/src/schema/loader-server.js +2 -1
  42. package/src/schema/runtime-browser-stubs.js +20 -9
  43. package/src/schema/runtime-ddl.js +161 -44
  44. package/src/schema/runtime-migrate.js +681 -0
  45. package/src/schema/runtime-orm.js +698 -54
  46. package/src/schema/runtime-validate.js +808 -24
  47. package/src/schema/runtime.generated.js +2395 -135
  48. package/src/schema/schema.js +1054 -94
  49. package/src/typecheck.js +1610 -127
  50. package/src/types.js +90 -6
  51. package/src/grammar/lunar.rip +0 -2412
  52. /package/docs/demo/{components → routes}/_layout.rip +0 -0
  53. /package/docs/demo/{components → routes}/about.rip +0 -0
  54. /package/docs/demo/{components → routes}/card.rip +0 -0
  55. /package/docs/demo/{components → routes}/counter.rip +0 -0
  56. /package/docs/demo/{components → routes}/index.rip +0 -0
  57. /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 ? expandSuffixes(type) : null, hasDefault });
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 ? expandSuffixes(type) : null, hasDefault });
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 ? expandSuffixes(type) : null);
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}: ${expandSuffixes(type)}` : paramName);
312
- if (type) fields.push({ name, type: expandSuffixes(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}: ${expandSuffixes(type)}` : `...${name}: any[]`);
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?.predicate;
381
+ let isOptional = hasDefault || tok.data?.optional;
368
382
  if (paramType) {
369
- params.push(`${paramName}${isOptional ? '?' : ''}: ${expandSuffixes(paramType)}`);
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 ? `: ${expandSuffixes(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 = expandSuffixes(data.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?.await === true) returnType = 'void';
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 ? `: ${expandSuffixes(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 ? `: ${expandSuffixes(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
- if (tag === 'IDENTIFIER' && !inClass &&
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 ? `: ${expandSuffixes(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 ? expandSuffixes(returnType) : 'any';
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 = expandSuffixes(t.data.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 = expandSuffixes(arrowToken.data.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
- // Suffix expansion Rip type suffixes to TypeScript
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 expandSuffixes(typeStr) {
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, hasDefault;
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
- hasDefault = true;
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 ? expandSuffixes(type) : (inferLiteralType(member[2]) || 'any');
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
- hasDefault = false;
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 ? expandSuffixes(inner.type) : 'any';
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 ? expandSuffixes(type) : 'any';
1064
- let opt = hasDefault ? '?' : '';
1065
- if (!hasDefault) hasRequired = true;
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}>;`);
@@ -1,27 +1,25 @@
1
- # Solar & Lunar Dual Parser Generators
1
+ # Solar — Rip's Parser Generator
2
2
 
3
- One grammar. Two parsers. Two fundamentally different parsing strategies.
3
+ One grammar. One parser. Mathematically derived.
4
4
 
5
5
  ```
6
- grammar.rip ──→ Solar ──→ parser.js (SLR(1) table-driven, 215KB)
7
- └→ Lunar ──→ parser-rd.js (predictive recursive descent, 110KB)
6
+ grammar.rip ──→ Solar ──→ parser.js (SLR(1) table-driven)
8
7
  ```
9
8
 
10
- Both parsers accept the same token stream from the lexer and produce identical
11
- s-expression ASTs. They share no code at runtimethey are completely
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 | Lines | Purpose |
21
- |------|-------|---------|
22
- | `grammar.rip` | 944 | Grammar specification — defines all syntax rules |
23
- | `solar.rip` | 929 | SLR(1) parser generator — produces table-driven parsers |
24
- | `lunar.rip` | 2,412 | Predictive recursive descent generator — produces PRD parsers |
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 801 states with all transitions delta-compressed for minimal size.
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
- ### Integration with Lunar
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
- Same grammar. Same semantics. Two fundamentally different parsers.
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`
@@ -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: when the inner expression is a single `=` assignment,
596
- # tag it with `.parenthesized = true` so the codegen ternary-hoist doesn't
597
- # silently turn `(x = "a") if cond else "b"` (conditional assign) into
598
- # `x = (cond ? "a" : "b")` (unconditional). The marker is harmless for any
599
- # other consumer; it's just a flag the hoist consults.
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]) && $2[0][0] === "=" && ($2[0].parenthesized = true), $2[0]) : ["block", ...$2]'
602
- o '( INDENT Body OUTDENT )', '$3.length === 1 ? (Array.isArray($3[0]) && $3[0][0] === "=" && ($3[0].parenthesized = true), $3[0]) : ["block", ...$3]'
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