rip-lang 3.7.4 → 3.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/components.js CHANGED
@@ -101,8 +101,11 @@ export function installComponentSupport(CodeGenerator) {
101
101
  }
102
102
  current = current[1];
103
103
  }
104
- const tag = typeof current === 'string' ? current : (current instanceof String ? current.valueOf() : 'div');
105
- return { tag, classes };
104
+ let raw = typeof current === 'string' ? current : (current instanceof String ? current.valueOf() : 'div');
105
+ // Split tag#id — e.g. "div#content" → tag: "div", id: "content"
106
+ let [tag, id] = raw.split('#');
107
+ if (!tag) tag = 'div'; // bare #id → div
108
+ return { tag, classes, id };
106
109
  };
107
110
 
108
111
  // ==========================================================================
@@ -133,6 +136,11 @@ export function installComponentSupport(CodeGenerator) {
133
136
  return sexpr;
134
137
  }
135
138
 
139
+ // Dot access: transform the object but not the property name
140
+ if (sexpr[0] === '.') {
141
+ return ['.', this.transformComponentMembers(sexpr[1]), sexpr[2]];
142
+ }
143
+
136
144
  // Force thin arrows to fat arrows inside components to preserve this binding
137
145
  if (sexpr[0] === '->') {
138
146
  return ['=>', ...sexpr.slice(1).map(item => this.transformComponentMembers(item))];
@@ -264,9 +272,9 @@ export function installComponentSupport(CodeGenerator) {
264
272
 
265
273
  // Effects
266
274
  for (const effect of effects) {
267
- const effectBody = effect[1];
275
+ const effectBody = effect[2];
268
276
  const effectCode = this.generateInComponent(effectBody, 'value');
269
- lines.push(` __effect(${effectCode});`);
277
+ lines.push(` __effect(() => { ${effectCode}; });`);
270
278
  }
271
279
 
272
280
  lines.push(' }');
@@ -276,8 +284,10 @@ export function installComponentSupport(CodeGenerator) {
276
284
  if (Array.isArray(func) && (func[0] === '->' || func[0] === '=>')) {
277
285
  const [, params, methodBody] = func;
278
286
  const paramStr = Array.isArray(params) ? params.map(p => this.formatParam(p)).join(', ') : '';
279
- const bodyCode = this.generateInComponent(methodBody, 'value');
280
- lines.push(` ${name}(${paramStr}) { return ${bodyCode}; }`);
287
+ const transformed = this.reactiveMembers ? this.transformComponentMembers(methodBody) : methodBody;
288
+ const isAsync = this.containsAwait(methodBody);
289
+ const bodyCode = this.generateFunctionBody(transformed, params || []);
290
+ lines.push(` ${isAsync ? 'async ' : ''}${name}(${paramStr}) ${bodyCode}`);
281
291
  }
282
292
  }
283
293
 
@@ -285,8 +295,10 @@ export function installComponentSupport(CodeGenerator) {
285
295
  for (const { name, value } of lifecycleHooks) {
286
296
  if (Array.isArray(value) && (value[0] === '->' || value[0] === '=>')) {
287
297
  const [, , hookBody] = value;
288
- const bodyCode = this.generateInComponent(hookBody, 'value');
289
- lines.push(` ${name}() { return ${bodyCode}; }`);
298
+ const transformed = this.reactiveMembers ? this.transformComponentMembers(hookBody) : hookBody;
299
+ const isAsync = this.containsAwait(hookBody);
300
+ const bodyCode = this.generateFunctionBody(transformed, []);
301
+ lines.push(` ${isAsync ? 'async ' : ''}${name}() ${bodyCode}`);
290
302
  }
291
303
  }
292
304
 
@@ -428,9 +440,11 @@ export function installComponentSupport(CodeGenerator) {
428
440
  this._setupLines.push(`__effect(() => { ${textVar}.data = this.${str}.value; });`);
429
441
  return textVar;
430
442
  }
431
- // Static tag without content
443
+ // Static tag without content (possibly with #id)
444
+ const [tagStr, idStr] = str.split('#');
432
445
  const elVar = this.newElementVar();
433
- this._createLines.push(`${elVar} = document.createElement('${str}');`);
446
+ this._createLines.push(`${elVar} = document.createElement('${tagStr || 'div'}');`);
447
+ if (idStr) this._createLines.push(`${elVar}.id = '${idStr}';`);
434
448
  return elVar;
435
449
  }
436
450
 
@@ -448,9 +462,10 @@ export function installComponentSupport(CodeGenerator) {
448
462
  return this.generateChildComponent(headStr, rest);
449
463
  }
450
464
 
451
- // HTML tag
465
+ // HTML tag (possibly with #id, e.g. div#content)
452
466
  if (headStr && this.isHtmlTag(headStr)) {
453
- return this.generateTag(headStr, [], rest);
467
+ let [tagName, id] = headStr.split('#');
468
+ return this.generateTag(tagName || 'div', [], rest, id);
454
469
  }
455
470
 
456
471
  // Property chain (div.class or item.name)
@@ -471,10 +486,10 @@ export function installComponentSupport(CodeGenerator) {
471
486
  return slotVar;
472
487
  }
473
488
 
474
- // HTML tag with classes (div.class)
475
- const { tag, classes } = this.collectTemplateClasses(sexpr);
489
+ // HTML tag with classes (div.class) and optional #id
490
+ const { tag, classes, id } = this.collectTemplateClasses(sexpr);
476
491
  if (tag && this.isHtmlTag(tag)) {
477
- return this.generateTag(tag, classes, []);
492
+ return this.generateTag(tag, classes, [], id);
478
493
  }
479
494
 
480
495
  // General property access (e.g., item.name in a loop)
@@ -494,13 +509,13 @@ export function installComponentSupport(CodeGenerator) {
494
509
  return this.generateDynamicTag(tag, classExprs, rest);
495
510
  }
496
511
 
497
- const { tag, classes } = this.collectTemplateClasses(head);
512
+ const { tag, classes, id } = this.collectTemplateClasses(head);
498
513
  if (tag && this.isHtmlTag(tag)) {
499
514
  // Dynamic class syntax: div.("classes") → (. div __clsx) "classes"
500
515
  if (classes.length === 1 && classes[0] === '__clsx') {
501
516
  return this.generateDynamicTag(tag, rest, []);
502
517
  }
503
- return this.generateTag(tag, classes, rest);
518
+ return this.generateTag(tag, classes, rest, id);
504
519
  }
505
520
  }
506
521
 
@@ -532,20 +547,12 @@ export function installComponentSupport(CodeGenerator) {
532
547
  };
533
548
 
534
549
  // --------------------------------------------------------------------------
535
- // generateTagHTML element with static classes and children
550
+ // appendChildrenshared child-processing loop for generateTag/generateDynamicTag
536
551
  // --------------------------------------------------------------------------
537
552
 
538
- proto.generateTag = function(tag, classes, args) {
539
- const elVar = this.newElementVar();
540
- this._createLines.push(`${elVar} = document.createElement('${tag}');`);
541
-
542
- if (classes.length > 0) {
543
- this._createLines.push(`${elVar}.className = '${classes.join(' ')}';`);
544
- }
545
-
553
+ proto.appendChildren = function(elVar, args) {
546
554
  for (const arg of args) {
547
- // Arrow function = children
548
- if (Array.isArray(arg) && (arg[0] === '->' || arg[0] === '=>')) {
555
+ if (this.is(arg, '->') || this.is(arg, '=>')) {
549
556
  const block = arg[2];
550
557
  if (this.is(block, 'block')) {
551
558
  for (const child of block.slice(1)) {
@@ -557,46 +564,47 @@ export function installComponentSupport(CodeGenerator) {
557
564
  this._createLines.push(`${elVar}.appendChild(${childVar});`);
558
565
  }
559
566
  }
560
- // Object = attributes/events
561
567
  else if (this.is(arg, 'object')) {
562
568
  this.generateAttributes(elVar, arg);
563
569
  }
564
- // String = text child
565
- else if (typeof arg === 'string') {
570
+ else if (typeof arg === 'string' || arg instanceof String) {
566
571
  const textVar = this.newTextVar();
567
- if (arg.startsWith('"') || arg.startsWith("'") || arg.startsWith('`')) {
568
- this._createLines.push(`${textVar} = document.createTextNode(${arg});`);
569
- } else if (this.reactiveMembers && this.reactiveMembers.has(arg)) {
570
- this._createLines.push(`${textVar} = document.createTextNode('');`);
571
- this._setupLines.push(`__effect(() => { ${textVar}.data = this.${arg}.value; });`);
572
- } else if (this.componentMembers && this.componentMembers.has(arg)) {
573
- this._createLines.push(`${textVar} = document.createTextNode(String(this.${arg}));`);
574
- } else {
575
- this._createLines.push(`${textVar} = document.createTextNode(String(${arg}));`);
576
- }
577
- this._createLines.push(`${elVar}.appendChild(${textVar});`);
578
- }
579
- // String object (from parser)
580
- else if (arg instanceof String) {
581
572
  const val = arg.valueOf();
582
- const textVar = this.newTextVar();
583
573
  if (val.startsWith('"') || val.startsWith("'") || val.startsWith('`')) {
584
574
  this._createLines.push(`${textVar} = document.createTextNode(${val});`);
585
575
  } else if (this.reactiveMembers && this.reactiveMembers.has(val)) {
586
576
  this._createLines.push(`${textVar} = document.createTextNode('');`);
587
577
  this._setupLines.push(`__effect(() => { ${textVar}.data = this.${val}.value; });`);
578
+ } else if (this.componentMembers && this.componentMembers.has(val)) {
579
+ this._createLines.push(`${textVar} = document.createTextNode(String(this.${val}));`);
588
580
  } else {
589
- this._createLines.push(`${textVar} = document.createTextNode(String(${val}));`);
581
+ this._createLines.push(`${textVar} = document.createTextNode(${this.generateInComponent(arg, 'value')});`);
590
582
  }
591
583
  this._createLines.push(`${elVar}.appendChild(${textVar});`);
592
584
  }
593
- // Other = nested element
594
585
  else if (arg) {
595
586
  const childVar = this.generateNode(arg);
596
587
  this._createLines.push(`${elVar}.appendChild(${childVar});`);
597
588
  }
598
589
  }
590
+ };
591
+
592
+ // --------------------------------------------------------------------------
593
+ // generateTag — HTML element with static classes and children
594
+ // --------------------------------------------------------------------------
599
595
 
596
+ proto.generateTag = function(tag, classes, args, id) {
597
+ const elVar = this.newElementVar();
598
+ this._createLines.push(`${elVar} = document.createElement('${tag}');`);
599
+
600
+ if (id) {
601
+ this._createLines.push(`${elVar}.id = '${id}';`);
602
+ }
603
+ if (classes.length > 0) {
604
+ this._createLines.push(`${elVar}.className = '${classes.join(' ')}';`);
605
+ }
606
+
607
+ this.appendChildren(elVar, args);
600
608
  return elVar;
601
609
  };
602
610
 
@@ -610,51 +618,13 @@ export function installComponentSupport(CodeGenerator) {
610
618
 
611
619
  if (classExprs.length > 0) {
612
620
  const classArgs = classExprs.map(e => this.generateInComponent(e, 'value')).join(', ');
613
- const hasReactive = classExprs.some(e => this.hasReactiveDeps(e));
614
- if (hasReactive) {
615
- this._setupLines.push(`__effect(() => { ${elVar}.className = __clsx(${classArgs}); });`);
616
- } else {
617
- this._createLines.push(`${elVar}.className = __clsx(${classArgs});`);
618
- }
619
- }
620
-
621
- for (const arg of children) {
622
- const argHead = Array.isArray(arg) ? (arg[0] instanceof String ? arg[0].valueOf() : arg[0]) : null;
623
- if (argHead === '->' || argHead === '=>') {
624
- const block = arg[2];
625
- const blockHead = Array.isArray(block) ? (block[0] instanceof String ? block[0].valueOf() : block[0]) : null;
626
- if (blockHead === 'block') {
627
- for (const child of block.slice(1)) {
628
- const childVar = this.generateNode(child);
629
- this._createLines.push(`${elVar}.appendChild(${childVar});`);
630
- }
631
- } else if (block) {
632
- const childVar = this.generateNode(block);
633
- this._createLines.push(`${elVar}.appendChild(${childVar});`);
634
- }
635
- }
636
- else if (this.is(arg, 'object')) {
637
- this.generateAttributes(elVar, arg);
638
- }
639
- else if (typeof arg === 'string' || arg instanceof String) {
640
- const textVar = this.newTextVar();
641
- const argStr = arg.valueOf();
642
- if (argStr.startsWith('"') || argStr.startsWith("'") || argStr.startsWith('`')) {
643
- this._createLines.push(`${textVar} = document.createTextNode(${argStr});`);
644
- } else if (this.reactiveMembers && this.reactiveMembers.has(argStr)) {
645
- this._createLines.push(`${textVar} = document.createTextNode('');`);
646
- this._setupLines.push(`__effect(() => { ${textVar}.data = this.${argStr}.value; });`);
647
- } else {
648
- this._createLines.push(`${textVar} = document.createTextNode(${this.generateInComponent(arg, 'value')});`);
649
- }
650
- this._createLines.push(`${elVar}.appendChild(${textVar});`);
651
- }
652
- else {
653
- const childVar = this.generateNode(arg);
654
- this._createLines.push(`${elVar}.appendChild(${childVar});`);
655
- }
621
+ // Dynamic classes are always wrapped in __effect — the .() syntax exists
622
+ // precisely for reactive class expressions. If a class were static, you'd
623
+ // just write div.foo.bar instead. The effect tracks signal reads at runtime.
624
+ this._setupLines.push(`__effect(() => { ${elVar}.className = __clsx(${classArgs}); });`);
656
625
  }
657
626
 
627
+ this.appendChildren(elVar, children);
658
628
  return elVar;
659
629
  };
660
630
 
@@ -688,6 +658,13 @@ export function installComponentSupport(CodeGenerator) {
688
658
  key = key.slice(1, -1);
689
659
  }
690
660
 
661
+ // Element ref: ref: "name" → this.name = element
662
+ if (key === 'ref') {
663
+ const refName = String(value).replace(/^["']|["']$/g, '');
664
+ this._createLines.push(`this.${refName} = ${elVar};`);
665
+ continue;
666
+ }
667
+
691
668
  // Two-way binding: __bind_value__ pattern
692
669
  if (key.startsWith(BIND_PREFIX) && key.endsWith(BIND_SUFFIX)) {
693
670
  const prop = key.slice(BIND_PREFIX.length, -BIND_SUFFIX.length);
@@ -1074,6 +1051,7 @@ export function installComponentSupport(CodeGenerator) {
1074
1051
 
1075
1052
  this._createLines.push(`${instVar} = new ${componentName}(${propsCode});`);
1076
1053
  this._createLines.push(`${elVar} = ${instVar}._create();`);
1054
+ this._createLines.push(`(this._children || (this._children = [])).push(${instVar});`);
1077
1055
 
1078
1056
  this._setupLines.push(`if (${instVar}._setup) ${instVar}._setup();`);
1079
1057
 
@@ -1098,7 +1076,12 @@ export function installComponentSupport(CodeGenerator) {
1098
1076
  for (let i = 1; i < arg.length; i++) {
1099
1077
  const [key, value] = arg[i];
1100
1078
  if (typeof key === 'string') {
1079
+ // Pass reactive members as signals (not values) for reactive prop binding.
1080
+ // Child's __state passthrough returns the signal as-is — shared reactivity.
1081
+ const prevReactive = this.reactiveMembers;
1082
+ this.reactiveMembers = new Set();
1101
1083
  const valueCode = this.generateInComponent(value, 'value');
1084
+ this.reactiveMembers = prevReactive;
1102
1085
  props.push(`${key}: ${valueCode}`);
1103
1086
  }
1104
1087
  }
@@ -1136,16 +1119,24 @@ export function installComponentSupport(CodeGenerator) {
1136
1119
  // --------------------------------------------------------------------------
1137
1120
 
1138
1121
  proto.hasReactiveDeps = function(sexpr) {
1139
- if (!this.reactiveMembers || this.reactiveMembers.size === 0) return false;
1140
-
1141
1122
  if (typeof sexpr === 'string') {
1142
- return this.reactiveMembers.has(sexpr);
1123
+ return !!(this.reactiveMembers && this.reactiveMembers.has(sexpr));
1143
1124
  }
1144
1125
 
1145
1126
  if (!Array.isArray(sexpr)) return false;
1146
1127
 
1128
+ // Direct this.X — check reactive members
1147
1129
  if (sexpr[0] === '.' && sexpr[1] === 'this' && typeof sexpr[2] === 'string') {
1148
- return this.reactiveMembers.has(sexpr[2]);
1130
+ return !!(this.reactiveMembers && this.reactiveMembers.has(sexpr[2]));
1131
+ }
1132
+
1133
+ // Property chain through this (e.g., this.router.path, this.app.data.count)
1134
+ // Props and members may hold reactive objects with signal-backed getters,
1135
+ // so treat deeper this.X.Y chains as potentially reactive. The effect
1136
+ // system handles actual tracking at runtime — wrapping a non-reactive
1137
+ // chain in __effect just means it runs once with no overhead.
1138
+ if (sexpr[0] === '.' && this._rootsAtThis(sexpr[1])) {
1139
+ return true;
1149
1140
  }
1150
1141
 
1151
1142
  for (const child of sexpr) {
@@ -1155,6 +1146,15 @@ export function installComponentSupport(CodeGenerator) {
1155
1146
  return false;
1156
1147
  };
1157
1148
 
1149
+ // _rootsAtThis — check if a property-access chain is rooted at 'this'
1150
+ // --------------------------------------------------------------------------
1151
+
1152
+ proto._rootsAtThis = function(sexpr) {
1153
+ if (typeof sexpr === 'string') return sexpr === 'this';
1154
+ if (!Array.isArray(sexpr) || sexpr[0] !== '.') return false;
1155
+ return this._rootsAtThis(sexpr[1]);
1156
+ };
1157
+
1158
1158
  // ==========================================================================
1159
1159
  // Component Runtime
1160
1160
  // ==========================================================================
@@ -1228,6 +1228,11 @@ class __Component {
1228
1228
  return this;
1229
1229
  }
1230
1230
  unmount() {
1231
+ if (this._children) {
1232
+ for (const child of this._children) {
1233
+ child.unmount();
1234
+ }
1235
+ }
1231
1236
  if (this.unmounted) this.unmounted();
1232
1237
  if (this._root && this._root.parentNode) {
1233
1238
  this._root.parentNode.removeChild(this._root);
@@ -0,0 +1,234 @@
1
+ # Solar & Lunar — Dual Parser Generators
2
+
3
+ One grammar. Two parsers. Two fundamentally different parsing strategies.
4
+
5
+ ```
6
+ grammar.rip ──→ Solar ──→ parser.js (SLR(1) table-driven, 215KB)
7
+ └→ Lunar ──→ parser-rd.js (predictive recursive descent, 110KB)
8
+ ```
9
+
10
+ Both parsers accept the same token stream from the lexer and produce identical
11
+ s-expression ASTs. They share no code at runtime — they 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%.
15
+
16
+ ---
17
+
18
+ ## Files
19
+
20
+ | File | Lines | Purpose |
21
+ |------|-------|---------|
22
+ | `grammar.rip` | 944 | Grammar specification — defines all syntax rules |
23
+ | `solar.rip` | 926 | SLR(1) parser generator — produces table-driven parsers |
24
+ | `lunar.rip` | 2,412 | Predictive recursive descent generator — produces PRD parsers |
25
+
26
+ ---
27
+
28
+ ## Grammar (`grammar.rip`)
29
+
30
+ The grammar defines Rip's syntax as production rules with semantic actions.
31
+ Each rule maps a pattern of tokens and nonterminals to an s-expression:
32
+
33
+ ```coffee
34
+ # Assignment: Assignable = Expression → ["=", target, value]
35
+ Assign: [
36
+ o 'Assignable = Expression' , '["=", 1, 3]'
37
+ o 'Assignable = INDENT Expression OUTDENT' , '["=", 1, 4]'
38
+ ]
39
+
40
+ # If/else: builds nested s-expression nodes
41
+ IfBlock: [
42
+ o 'IF Expression Block' , '["if", 2, 3]'
43
+ o 'IfBlock ELSE IF Expression Block' , '...' # left-recursive chain
44
+ ]
45
+
46
+ # Binary operators: Expression OP Expression with precedence
47
+ Operation: [
48
+ o 'Expression + Expression' , '["+", 1, 3]'
49
+ o 'Expression MATH Expression' , '[2, 1, 3]'
50
+ o 'Expression ** Expression' , '["**", 1, 3]'
51
+ ]
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Solar (`solar.rip`) — SLR(1) Table Parser Generator
57
+
58
+ Solar processes the grammar through a classic compiler-theory pipeline:
59
+
60
+ ```
61
+ Grammar → Process Rules → Build LR Automaton → Compute FIRST/FOLLOW
62
+ → Build Parse Table → Resolve Conflicts → Generate Code
63
+ ```
64
+
65
+ ### What it produces
66
+
67
+ A table-driven parser where every parsing decision is a lookup:
68
+ `parseTable[state][symbol]` → shift, reduce, or accept. The parse table
69
+ encodes 801 states with all transitions delta-compressed for minimal size.
70
+
71
+ ### How to use
72
+
73
+ ```bash
74
+ 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
+ bun src/grammar/solar.rip --info grammar.rip # Show grammar statistics
77
+ bun src/grammar/solar.rip --sexpr grammar.rip # Show grammar as s-expression
78
+ ```
79
+
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, Whens |
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.
233
+
234
+ Same grammar. Same semantics. Two fundamentally different parsers.