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/CHANGELOG.md +97 -1
- package/README.md +42 -34
- package/docs/RIP-INTERNALS.md +2 -4
- package/docs/RIP-LANG.md +150 -3
- package/docs/RIP-TYPES.md +1 -2
- package/docs/demo.html +342 -0
- package/docs/dist/rip-ui.min.js +516 -0
- package/docs/dist/rip-ui.min.js.br +0 -0
- package/docs/dist/rip.browser.js +285 -406
- package/docs/dist/rip.browser.min.js +166 -204
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/docs/dist/ui.js +956 -0
- package/docs/dist/ui.min.js +2 -0
- package/docs/dist/ui.min.js.br +0 -0
- package/docs/dist/ui.rip +957 -0
- package/docs/dist/ui.rip.br +0 -0
- package/docs/examples.rip +180 -0
- package/docs/index.html +3 -1599
- package/docs/playground-app.html +1022 -0
- package/docs/playground-js.html +1645 -0
- package/docs/playground-rip-ui.html +1419 -0
- package/docs/playground-rip.html +1450 -0
- package/docs/rip-fav.svg +5 -0
- package/package.json +3 -3
- package/scripts/serve.js +3 -2
- package/src/browser.js +21 -5
- package/src/compiler.js +148 -221
- package/src/components.js +100 -95
- package/src/grammar/README.md +234 -0
- package/src/grammar/lunar.rip +2412 -0
- package/src/grammar/solar.rip +18 -4
- package/src/lexer.js +53 -24
- package/src/parser-rd.js +3242 -0
- package/src/parser.js +6 -5
- package/src/repl.js +24 -5
- package/docs/NOTES.md +0 -93
- package/docs/RIP-GUIDE.md +0 -698
- package/docs/RIP-REACTIVITY.md +0 -311
package/src/components.js
CHANGED
|
@@ -101,8 +101,11 @@ export function installComponentSupport(CodeGenerator) {
|
|
|
101
101
|
}
|
|
102
102
|
current = current[1];
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
|
|
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[
|
|
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
|
|
280
|
-
|
|
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
|
|
289
|
-
|
|
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('${
|
|
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
|
-
|
|
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
|
-
//
|
|
550
|
+
// appendChildren — shared child-processing loop for generateTag/generateDynamicTag
|
|
536
551
|
// --------------------------------------------------------------------------
|
|
537
552
|
|
|
538
|
-
proto.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
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.
|