rip-lang 3.7.3 → 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.
@@ -10,6 +10,7 @@
10
10
  import * as fs from 'fs'
11
11
  import * as path from 'path'
12
12
  import { fileURLToPath, pathToFileURL } from 'url'
13
+ import { install as installLunar } from './lunar.rip'
13
14
 
14
15
  VERSION = '1.5.0'
15
16
 
@@ -89,9 +90,11 @@ class Generator
89
90
  # Build parser
90
91
  @timing '💥 Total time', =>
91
92
  @timing 'processGrammar' , => @processGrammar grammar # Process grammar rules
92
- @timing 'buildLRAutomaton' , => @buildLRAutomaton() # Build LR(0) automaton
93
+ unless @options.rd
94
+ @timing 'buildLRAutomaton' , => @buildLRAutomaton() # Build LR(0) automaton
93
95
  @timing 'processLookaheads', => @processLookaheads() # Compute FIRST/FOLLOW and assign lookaheads
94
- @timing 'buildParseTable' , => @buildParseTable() # Build parse table with default actions
96
+ unless @options.rd
97
+ @timing 'buildParseTable' , => @buildParseTable() # Build parse table with default actions
95
98
 
96
99
  # ============================================================================
97
100
  # Helper Functions
@@ -470,6 +473,7 @@ class Generator
470
473
 
471
474
  # Assign FOLLOW sets to reduction items
472
475
  _assignItemLookaheads!: ->
476
+ return unless @states
473
477
  for state in @states
474
478
  for item as state.reductions
475
479
  follows = @types[item.rule.type]?.follows
@@ -653,6 +657,8 @@ class Generator
653
657
  # Decoder reconstructs [{key: val}, ...] at runtime
654
658
  "(()=>{let d=[#{data.join ','}],t=[],p=0,n,o,k,a;while(p<d.length){n=d[p++];o={};k=0;a=[];while(n--)k+=d[p++],a.push(k);for(k of a)o[k]=d[p++];t.push(o)}return t})()"
655
659
 
660
+ # Recursive descent generation — installed by Lunar (see lunar.rip)
661
+
656
662
  # ============================================================================
657
663
  # Runtime Parser
658
664
  # ============================================================================
@@ -767,6 +773,8 @@ class Generator
767
773
  # Exports
768
774
  # ==============================================================================
769
775
 
776
+ installLunar Generator
777
+
770
778
  export { Generator }
771
779
 
772
780
  # ==============================================================================
@@ -798,6 +806,7 @@ if isRunAsScript
798
806
  -v, --version Show version
799
807
  -i, --info Show grammar information
800
808
  -s, --sexpr Show grammar as s-expression
809
+ -r, --rd Generate recursive descent parser (parser-rd.js)
801
810
  -c, --conflicts Show conflict details (use with --info)
802
811
  -o, --output <file> Output file (default: parser.js)
803
812
 
@@ -835,7 +844,7 @@ if isRunAsScript
835
844
  console.log " Resolution: #{conflict.resolution} (by default)"
836
845
 
837
846
  # Parse command line
838
- options = {help: false, version: false, info: false, sexpr: false, conflicts: false, output: 'parser.js'}
847
+ options = {help: false, version: false, info: false, sexpr: false, conflicts: false, rd: false, output: 'parser.js'}
839
848
  grammarFile = null
840
849
  i = 0
841
850
 
@@ -846,6 +855,7 @@ if isRunAsScript
846
855
  when '-v', '--version' then options.version = true
847
856
  when '-i', '--info' then options.info = true
848
857
  when '-s', '--sexpr' then options.sexpr = true
858
+ when '-r', '--rd' then options.rd = true
849
859
  when '-c', '--conflicts' then options.conflicts = true
850
860
  when '-o', '--output' then options.output = process.argv[++i + 2]
851
861
  else grammarFile = arg unless arg.startsWith('-')
@@ -906,7 +916,11 @@ if isRunAsScript
906
916
  if options.info
907
917
  showStats generator
908
918
  else
909
- parserCode = generator.generate()
919
+ if options.rd
920
+ options.output = 'parser-rd.js' if options.output is 'parser.js'
921
+ parserCode = generator.generateRD()
922
+ else
923
+ parserCode = generator.generate()
910
924
  fs.writeFileSync options.output, parserCode
911
925
  console.log "\nParser generated: #{options.output}"
912
926
 
package/src/lexer.js CHANGED
@@ -220,7 +220,7 @@ let NEWLINE_RE = /^(?:\n[^\n\S]*)+/;
220
220
  let COMMENT_RE = /^(\s*)###([^#][\s\S]*?)(?:###([^\n\S]*)|###$)|^((?:\s*#(?!##[^#]).*)+)/;
221
221
  let CODE_RE = /^[-=]>/;
222
222
  let REACTIVE_RE = /^(?:~[=>]|=!)/;
223
- let STRING_START_RE = /^(?:'''|"""|'|")/;
223
+ let STRING_START_RE = /^(?:'''\\|"""\\|'''|"""|'|")/;
224
224
  let STRING_SINGLE_RE = /^(?:[^\\']|\\[\s\S])*/;
225
225
  let STRING_DOUBLE_RE = /^(?:[^\\"#$]|\\[\s\S]|\#(?!\{)|\$(?!\{))*/;
226
226
  let HEREDOC_SINGLE_RE = /^(?:[^\\']|\\[\s\S]|'(?!''))*/;
@@ -627,10 +627,19 @@ export class Lexer {
627
627
  // --------------------------------------------------------------------------
628
628
 
629
629
  commentToken() {
630
+ // In render blocks, #word is an element ID, not a comment
631
+ if (this.inRenderBlock) {
632
+ if (/^#[a-zA-Z_]/.test(this.chunk)) {
633
+ let prev = this.prev();
634
+ if (prev && (prev[0] === 'IDENTIFIER' || prev[0] === 'PROPERTY'))
635
+ return 0; // after a tag (div#main) → let # become a token for rewriter
636
+ let m = /^#([a-zA-Z_][\w-]*)/.exec(this.chunk);
637
+ if (m) { this.emit('IDENTIFIER', 'div#' + m[1]); return m[0].length; }
638
+ }
639
+ if (/^\s+#[a-zA-Z_]/.test(this.chunk)) return 0; // let lineToken handle indentation first
640
+ }
630
641
  let match = COMMENT_RE.exec(this.chunk);
631
642
  if (!match) return 0;
632
- // For now, consume the comment and discard it
633
- // TODO: attach comments to adjacent tokens for source map support
634
643
  return match[0].length;
635
644
  }
636
645
 
@@ -779,6 +788,8 @@ export class Lexer {
779
788
  if (!m) return 0;
780
789
 
781
790
  let quote = m[0];
791
+ let raw = quote.length > 1 && quote.endsWith('\\');
792
+ let baseQuote = raw ? quote.slice(0, -1) : quote;
782
793
  let prev = this.prev();
783
794
 
784
795
  // Tag 'from' in import/export context
@@ -787,24 +798,24 @@ export class Lexer {
787
798
  }
788
799
 
789
800
  let regex;
790
- switch (quote) {
801
+ switch (baseQuote) {
791
802
  case "'": regex = STRING_SINGLE_RE; break;
792
803
  case '"': regex = STRING_DOUBLE_RE; break;
793
804
  case "'''": regex = HEREDOC_SINGLE_RE; break;
794
805
  case '"""': regex = HEREDOC_DOUBLE_RE; break;
795
806
  }
796
807
 
797
- let {tokens: parts, index: end} = this.matchWithInterpolations(regex, quote);
798
- let heredoc = quote.length === 3;
808
+ let {tokens: parts, index: end} = this.matchWithInterpolations(regex, quote, baseQuote);
809
+ let heredoc = baseQuote.length === 3;
799
810
 
800
811
  // Heredoc indent processing
801
812
  let indent = null;
802
813
  if (heredoc) {
803
- indent = this.processHeredocIndent(end, quote, parts);
814
+ indent = this.processHeredocIndent(end, baseQuote, parts);
804
815
  }
805
816
 
806
817
  // Merge interpolation tokens into the stream
807
- this.mergeInterpolationTokens(parts, {quote, indent, endOffset: end});
818
+ this.mergeInterpolationTokens(parts, {quote: baseQuote, indent, endOffset: end, raw});
808
819
 
809
820
  return end;
810
821
  }
@@ -910,7 +921,7 @@ export class Lexer {
910
921
  }
911
922
 
912
923
  // Merge NEOSTRING/TOKENS into the real token stream
913
- mergeInterpolationTokens(tokens, {quote, indent, endOffset}) {
924
+ mergeInterpolationTokens(tokens, {quote, indent, endOffset, raw}) {
914
925
  if (tokens.length > 1) {
915
926
  this.emit('STRING_START', '(', {len: quote?.length || 0, data: {quote}});
916
927
  }
@@ -939,6 +950,12 @@ export class Lexer {
939
950
  processed = processed.replace(/\n[^\S\n]*$/, '');
940
951
  }
941
952
 
953
+ // Raw heredocs ('''\, """\): escape only recognized JS escape sequences
954
+ // so \n \t \u etc. stay literal, but \s \w \d pass through unchanged
955
+ if (raw) {
956
+ processed = processed.replace(/\\([nrtbfv0\\'"`xu])/g, '\\\\$1');
957
+ }
958
+
942
959
  this.emit('STRING', `"${processed}"`, {len: val.length, data: {quote}});
943
960
  }
944
961
  }
@@ -996,8 +1013,8 @@ export class Lexer {
996
1013
  let end = index + flags.length;
997
1014
 
998
1015
  if (parts.length === 1 || !parts.some(p => p[0] === 'TOKENS')) {
999
- // Simple heregex (no interpolations)
1000
- let body = parts[0]?.[1] || '';
1016
+ // Simple heregex (no interpolations) — escape unescaped / for regex literal
1017
+ let body = (parts[0]?.[1] || '').replace(/(?<!\\)\//g, '\\/');
1001
1018
  this.emit('REGEX', `/${body}/${flags}`, {len: end, data: {delimiter: '///', heregex: {flags}}});
1002
1019
  } else {
1003
1020
  // Complex heregex with interpolations
@@ -1133,10 +1150,23 @@ export class Lexer {
1133
1150
  else if (val === '~>') tag = 'REACT_ASSIGN';
1134
1151
  else if (val === '=!') tag = 'READONLY_ASSIGN';
1135
1152
  // Merge assignment: *config = {a: 1} → Object.assign(config, {a: 1})
1153
+ // Also supports *@ = props → Object.assign(this, props)
1136
1154
  else if (val === '*' && (!prev || prev[0] === 'TERMINATOR' || prev[0] === 'INDENT' || prev[0] === 'OUTDENT') &&
1137
- /^[a-zA-Z_$]/.test(this.chunk[1] || '')) {
1138
- // Scan ahead to find "IDENTIFIER =" pattern
1155
+ (/^[a-zA-Z_$]/.test(this.chunk[1] || '') || this.chunk[1] === '@')) {
1139
1156
  let rest = this.chunk.slice(1);
1157
+ // Handle *@ = ... → Object.assign(@, ...)
1158
+ let mAt = /^@(\s*)=(?!=)/.exec(rest);
1159
+ if (mAt) {
1160
+ let space = mAt[1];
1161
+ this.emit('IDENTIFIER', 'Object');
1162
+ this.emit('.', '.');
1163
+ let t = this.emit('PROPERTY', 'assign');
1164
+ t.spaced = true; // trigger implicit call detection
1165
+ this.emit('@', '@');
1166
+ this.emit(',', ',');
1167
+ return 1 + 1 + space.length + 1; // consume *@ =, value tokens become args
1168
+ }
1169
+ // Scan ahead to find "IDENTIFIER =" pattern
1140
1170
  let m = /^((?:(?!\s)[$\w\x7f-\uffff])+(?:\.[a-zA-Z_$][\w]*)*)(\s*)=(?!=)/.exec(rest);
1141
1171
  if (m) {
1142
1172
  let target = m[1], space = m[2];
@@ -1361,7 +1391,7 @@ export class Lexer {
1361
1391
  // - Combine #id selectors: div # main → div#main
1362
1392
  // - Two-way binding: value <=> username → __bind_value__: username
1363
1393
  // - Event modifiers: @click.prevent: → [@click.prevent]:
1364
- // - Dynamic classes: div.('card', x && 'active') → div.__cx__(...)
1394
+ // - Dynamic classes: div.('card', x && 'active') → div.__clsx(...)
1365
1395
  // - Implicit nesting: inject -> before INDENT for template elements
1366
1396
  // - Hyphenated attributes: data-foo: "x" → "data-foo": "x"
1367
1397
  // =========================================================================
@@ -1385,7 +1415,7 @@ export class Lexer {
1385
1415
  return isHtmlTag(name) || isComponent(name);
1386
1416
  };
1387
1417
 
1388
- let startsWithHtmlTag = (tokens, i) => {
1418
+ let startsWithTag = (tokens, i) => {
1389
1419
  let j = i;
1390
1420
  while (j > 0) {
1391
1421
  let pt = tokens[j - 1][0];
@@ -1394,7 +1424,7 @@ export class Lexer {
1394
1424
  }
1395
1425
  j--;
1396
1426
  }
1397
- return tokens[j] && tokens[j][0] === 'IDENTIFIER' && isHtmlTag(tokens[j][1]);
1427
+ return tokens[j] && tokens[j][0] === 'IDENTIFIER' && isTemplateTag(tokens[j][1]);
1398
1428
  };
1399
1429
 
1400
1430
  this.scanTokens(function(token, i, tokens) {
@@ -1486,7 +1516,7 @@ export class Lexer {
1486
1516
  if (tag === 'IDENTIFIER' || tag === 'PROPERTY') {
1487
1517
  let next = tokens[i + 1];
1488
1518
  let nextNext = tokens[i + 2];
1489
- if (next && next[0] === '#' && nextNext && nextNext[0] === 'PROPERTY') {
1519
+ if (next && next[0] === '#' && nextNext && (nextNext[0] === 'PROPERTY' || nextNext[0] === 'IDENTIFIER')) {
1490
1520
  token[1] = token[1] + '#' + nextNext[1];
1491
1521
  if (nextNext.spaced) token.spaced = true;
1492
1522
  tokens.splice(i + 1, 2);
@@ -1533,15 +1563,15 @@ export class Lexer {
1533
1563
 
1534
1564
  // ─────────────────────────────────────────────────────────────────────
1535
1565
  // Dynamic classes
1536
- // div.('card', x && 'active') → div.__cx__('card', x && 'active')
1537
- // .('card') → div.__cx__('card')
1566
+ // div.('card', x && 'active') → div.__clsx('card', x && 'active')
1567
+ // .('card') → div.__clsx('card')
1538
1568
  // ─────────────────────────────────────────────────────────────────────
1539
1569
  if (tag === '.' && nextToken && nextToken[0] === '(') {
1540
1570
  let prevToken = i > 0 ? tokens[i - 1] : null;
1541
1571
  let prevTag = prevToken ? prevToken[0] : null;
1542
1572
  let atLineStart = prevTag === 'INDENT' || prevTag === 'TERMINATOR';
1543
1573
 
1544
- let cxToken = gen('PROPERTY', '__cx__', token);
1574
+ let cxToken = gen('PROPERTY', '__clsx', token);
1545
1575
  nextToken[0] = 'CALL_START';
1546
1576
  let depth = 1;
1547
1577
  for (let j = i + 2; j < tokens.length && depth > 0; j++) {
@@ -1572,14 +1602,16 @@ export class Lexer {
1572
1602
  }
1573
1603
 
1574
1604
  let isTemplateElement = false;
1605
+ let prevTag = i > 0 ? tokens[i - 1][0] : null;
1606
+ let isAfterControlFlow = prevTag === 'IF' || prevTag === 'UNLESS' || prevTag === 'WHILE' || prevTag === 'UNTIL' || prevTag === 'WHEN';
1575
1607
 
1576
- if (tag === 'IDENTIFIER' && isTemplateTag(token[1])) {
1608
+ if (tag === 'IDENTIFIER' && isTemplateTag(token[1]) && !isAfterControlFlow) {
1577
1609
  isTemplateElement = true;
1578
1610
  } else if (tag === 'PROPERTY' || tag === 'STRING' || tag === 'CALL_END' || tag === ')') {
1579
- isTemplateElement = startsWithHtmlTag(tokens, i);
1611
+ isTemplateElement = startsWithTag(tokens, i);
1580
1612
  }
1581
1613
  else if (tag === 'IDENTIFIER' && i > 1 && tokens[i - 1][0] === '...') {
1582
- if (startsWithHtmlTag(tokens, i)) {
1614
+ if (startsWithTag(tokens, i)) {
1583
1615
  let commaToken = gen(',', ',', token);
1584
1616
  let arrowToken = gen('->', '->', token);
1585
1617
  arrowToken.newLine = true;
@@ -1589,16 +1621,36 @@ export class Lexer {
1589
1621
  }
1590
1622
 
1591
1623
  if (isTemplateElement) {
1592
- let callStartToken = gen('CALL_START', '(', token);
1593
- let arrowToken = gen('->', '->', token);
1594
- arrowToken.newLine = true;
1595
-
1596
- tokens.splice(i + 1, 0, callStartToken, arrowToken);
1597
- pendingCallEnds.push(currentIndent + 1);
1598
- return 3;
1624
+ let isClassOrIdTail = tag === 'PROPERTY' && i > 0 && (tokens[i - 1][0] === '.' || tokens[i - 1][0] === '#');
1625
+ if ((tag === 'IDENTIFIER' && isTemplateTag(token[1])) || isClassOrIdTail) {
1626
+ // Bare tag or tag.class/tag#id (no other args): inject CALL_START -> and manage CALL_END
1627
+ let callStartToken = gen('CALL_START', '(', token);
1628
+ let arrowToken = gen('->', '->', token);
1629
+ arrowToken.newLine = true;
1630
+ tokens.splice(i + 1, 0, callStartToken, arrowToken);
1631
+ pendingCallEnds.push(currentIndent + 1);
1632
+ return 3;
1633
+ } else {
1634
+ // Tag with args: inject , -> (call wrapping handled by addImplicitBracesAndParens)
1635
+ let commaToken = gen(',', ',', token);
1636
+ let arrowToken = gen('->', '->', token);
1637
+ arrowToken.newLine = true;
1638
+ tokens.splice(i + 1, 0, commaToken, arrowToken);
1639
+ return 3;
1640
+ }
1599
1641
  }
1600
1642
  }
1601
1643
 
1644
+ // ─────────────────────────────────────────────────────────────────────
1645
+ // Bare component reference (PascalCase, no children, no args)
1646
+ // Counter → Counter() so it gets treated as a component instantiation
1647
+ // ─────────────────────────────────────────────────────────────────────
1648
+ if (tag === 'IDENTIFIER' && isComponent(token[1]) &&
1649
+ nextToken && (nextToken[0] === 'OUTDENT' || nextToken[0] === 'TERMINATOR')) {
1650
+ tokens.splice(i + 1, 0, gen('CALL_START', '(', token), gen('CALL_END', ')', token));
1651
+ return 3;
1652
+ }
1653
+
1602
1654
  return 1;
1603
1655
  });
1604
1656
  }