rip-lang 3.13.133 → 3.13.135

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/error.js ADDED
@@ -0,0 +1,250 @@
1
+ // RipError — structured diagnostics for the Rip compiler
2
+ //
3
+ // Unifies error reporting across lexer, parser, and codegen with source
4
+ // locations, contextual snippets, and carets. Consumers (CLI, loader, browser,
5
+ // REPL, server) call format() for terminal output or formatHTML() for browser.
6
+
7
+ export class RipError extends Error {
8
+ constructor(message, {
9
+ code = null, // e.g. 'E_SYNTAX', 'E_CODEGEN', 'E_PARSE'
10
+ file = null, // source filename
11
+ line = null, // 0-based line number
12
+ column = null, // 0-based column number
13
+ length = 1, // length of the offending span
14
+ source = null, // full original source text
15
+ suggestion = null,
16
+ phase = null, // 'lexer', 'parser', 'codegen'
17
+ } = {}) {
18
+ super(message);
19
+ this.name = 'RipError';
20
+ this.code = code;
21
+ this.file = file;
22
+ this.line = line;
23
+ this.column = column;
24
+ this.length = length;
25
+ this.source = source;
26
+ this.suggestion = suggestion;
27
+ this.phase = phase;
28
+ }
29
+
30
+ // Construct from a lexer SyntaxError (has .location)
31
+ static fromLexer(err, source, file) {
32
+ let loc = err.location || {};
33
+ return new RipError(err.message, {
34
+ code: 'E_SYNTAX',
35
+ file,
36
+ line: loc.first_line ?? null,
37
+ column: loc.first_column ?? null,
38
+ length: loc.last_column != null && loc.first_column != null
39
+ ? loc.last_column - loc.first_column + 1 : 1,
40
+ source,
41
+ phase: 'lexer',
42
+ });
43
+ }
44
+
45
+ // Construct from a parser Error (has .hash with line, loc, token, expected)
46
+ static fromParser(err, source, file) {
47
+ let h = err.hash || {};
48
+ let loc = h.loc || {};
49
+ let line = h.line ?? loc.r ?? null;
50
+ let column = loc.first_column ?? loc.c ?? null;
51
+ let suggestion = null;
52
+ if (h.expected?.length) {
53
+ let first5 = h.expected.slice(0, 5).map(e => e.replace(/'/g, ''));
54
+ suggestion = `Expected ${first5.join(', ')}`;
55
+ if (h.expected.length > 5) suggestion += `, ... (${h.expected.length} total)`;
56
+ }
57
+ // Build a clean message from the hash instead of using the parser's pre-formatted string
58
+ let token = h.token || 'token';
59
+ let near = h.text ? ` near '${h.text}'` : '';
60
+ let message = `Unexpected ${token}${near}`;
61
+ return new RipError(message, {
62
+ code: 'E_PARSE',
63
+ file,
64
+ line,
65
+ column,
66
+ length: h.text?.length || 1,
67
+ source,
68
+ suggestion,
69
+ phase: 'parser',
70
+ });
71
+ }
72
+
73
+ // Construct from an s-expression node's .loc in the codegen phase
74
+ static fromSExpr(message, sexpr, source, file, suggestion) {
75
+ let loc = sexpr?.loc || {};
76
+ return new RipError(message, {
77
+ code: 'E_CODEGEN',
78
+ file,
79
+ line: loc.r ?? null,
80
+ column: loc.c ?? null,
81
+ length: loc.n ?? 1,
82
+ source,
83
+ suggestion,
84
+ phase: 'codegen',
85
+ });
86
+ }
87
+
88
+ // Human-readable location string: "file.rip:3:5" or "3:5" or ""
89
+ get locationString() {
90
+ let parts = [];
91
+ if (this.file) parts.push(this.file);
92
+ if (this.line != null) {
93
+ parts.push(`${this.line + 1}:${(this.column ?? 0) + 1}`);
94
+ }
95
+ return parts.join(':');
96
+ }
97
+
98
+ // ---- Terminal formatter ----
99
+
100
+ format({ color = true } = {}) {
101
+ let c = color ? {
102
+ red: '\x1b[31m',
103
+ yellow: '\x1b[33m',
104
+ cyan: '\x1b[36m',
105
+ dim: '\x1b[2m',
106
+ bold: '\x1b[1m',
107
+ reset: '\x1b[0m',
108
+ } : { red: '', yellow: '', cyan: '', dim: '', bold: '', reset: '' };
109
+
110
+ let lines = [];
111
+
112
+ // Header: error message
113
+ let loc = this.locationString;
114
+ let header = loc ? `${c.cyan}${loc}${c.reset} ` : '';
115
+ lines.push(`${header}${c.red}${c.bold}error${c.reset}${c.bold}: ${this.message}${c.reset}`);
116
+
117
+ // Source snippet with caret
118
+ let snippet = this._snippet();
119
+ if (snippet) {
120
+ lines.push('');
121
+ for (let s of snippet) {
122
+ if (s.type === 'source') {
123
+ lines.push(`${c.dim}${s.gutter}${c.reset}${s.text}`);
124
+ } else if (s.type === 'caret') {
125
+ lines.push(`${c.dim}${s.gutter}${c.reset}${c.red}${c.bold}${s.text}${c.reset}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ // Suggestion
131
+ if (this.suggestion) {
132
+ lines.push('');
133
+ lines.push(`${c.yellow}hint${c.reset}: ${this.suggestion}`);
134
+ }
135
+
136
+ return lines.join('\n');
137
+ }
138
+
139
+ // ---- HTML formatter ----
140
+
141
+ formatHTML() {
142
+ let lines = [];
143
+ lines.push('<div class="rip-error">');
144
+ lines.push('<style>');
145
+ lines.push(`.rip-error { font-family: ui-monospace, "SF Mono", Menlo, Monaco, monospace; font-size: 13px; line-height: 1.5; padding: 16px 20px; background: #1e1e2e; color: #cdd6f4; border-radius: 8px; overflow-x: auto; }`);
146
+ lines.push(`.rip-error .re-header { color: #f38ba8; font-weight: 600; }`);
147
+ lines.push(`.rip-error .re-loc { color: #89b4fa; }`);
148
+ lines.push(`.rip-error .re-gutter { color: #585b70; user-select: none; }`);
149
+ lines.push(`.rip-error .re-caret { color: #f38ba8; font-weight: 700; }`);
150
+ lines.push(`.rip-error .re-hint { color: #f9e2af; }`);
151
+ lines.push(`.rip-error .re-snippet { margin: 8px 0; }`);
152
+ lines.push('</style>');
153
+
154
+ let loc = this.locationString;
155
+ let locSpan = loc ? `<span class="re-loc">${esc(loc)}</span> ` : '';
156
+ lines.push(`<div class="re-header">${locSpan}error: ${esc(this.message)}</div>`);
157
+
158
+ let snippet = this._snippet();
159
+ if (snippet) {
160
+ lines.push('<pre class="re-snippet">');
161
+ for (let s of snippet) {
162
+ if (s.type === 'source') {
163
+ lines.push(`<span class="re-gutter">${esc(s.gutter)}</span>${esc(s.text)}`);
164
+ } else if (s.type === 'caret') {
165
+ lines.push(`<span class="re-gutter">${esc(s.gutter)}</span><span class="re-caret">${esc(s.text)}</span>`);
166
+ }
167
+ }
168
+ lines.push('</pre>');
169
+ }
170
+
171
+ if (this.suggestion) {
172
+ lines.push(`<div class="re-hint">hint: ${esc(this.suggestion)}</div>`);
173
+ }
174
+
175
+ lines.push('</div>');
176
+ return lines.join('\n');
177
+ }
178
+
179
+ // ---- Snippet builder (shared by format and formatHTML) ----
180
+
181
+ _snippet() {
182
+ if (this.source == null || this.line == null) return null;
183
+
184
+ let sourceLines = this.source.split('\n');
185
+ let errLine = this.line;
186
+ if (errLine < 0 || errLine >= sourceLines.length) return null;
187
+
188
+ let contextRadius = 2;
189
+ let start = Math.max(0, errLine - contextRadius);
190
+ let end = Math.min(sourceLines.length - 1, errLine + contextRadius);
191
+ let gutterWidth = String(end + 1).length;
192
+
193
+ let result = [];
194
+
195
+ for (let i = start; i <= end; i++) {
196
+ let lineNum = String(i + 1).padStart(gutterWidth);
197
+ let gutter = ` ${lineNum} │ `;
198
+ result.push({ type: 'source', gutter, text: sourceLines[i] });
199
+
200
+ if (i === errLine && this.column != null) {
201
+ let pad = ' '.repeat(this.column);
202
+ let caretLen = Math.max(1, Math.min(this.length || 1, sourceLines[i].length - this.column));
203
+ let carets = '^'.repeat(caretLen);
204
+ let emptyGutter = ' '.repeat(gutterWidth + 2) + '│ ';
205
+ result.push({ type: 'caret', gutter: emptyGutter, text: `${pad}${carets}` });
206
+ }
207
+ }
208
+
209
+ return result;
210
+ }
211
+ }
212
+
213
+ // Detect whether an error is a lexer SyntaxError with .location
214
+ export function isLexerError(err) {
215
+ return err instanceof SyntaxError && err.location != null;
216
+ }
217
+
218
+ // Detect whether an error is a parser error with .hash
219
+ export function isParserError(err) {
220
+ return !(err instanceof SyntaxError) && err.hash != null;
221
+ }
222
+
223
+ // Upgrade any error to RipError (idempotent on RipError instances)
224
+ export function toRipError(err, source, file) {
225
+ if (err instanceof RipError) {
226
+ if (file && !err.file) err.file = file;
227
+ if (source && !err.source) err.source = source;
228
+ return err;
229
+ }
230
+ if (isLexerError(err)) return RipError.fromLexer(err, source, file);
231
+ if (isParserError(err)) return RipError.fromParser(err, source, file);
232
+ // Unknown error — wrap with no location
233
+ return new RipError(err.message, { file, source, phase: 'unknown' });
234
+ }
235
+
236
+ // Format any error for terminal display (works on RipError and plain Error)
237
+ export function formatError(err, { source, file, color = true } = {}) {
238
+ let re = (err instanceof RipError) ? err : toRipError(err, source, file);
239
+ return re.format({ color });
240
+ }
241
+
242
+ // Format any error for HTML display
243
+ export function formatErrorHTML(err, { source, file } = {}) {
244
+ let re = (err instanceof RipError) ? err : toRipError(err, source, file);
245
+ return re.formatHTML();
246
+ }
247
+
248
+ function esc(s) {
249
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
250
+ }
@@ -70,7 +70,6 @@ grammar =
70
70
  o 'Code'
71
71
  o 'Operation'
72
72
  o 'Assign'
73
- o 'RightwardAssign'
74
73
  o 'ReactiveAssign'
75
74
  o 'ComputedAssign'
76
75
  o 'ReadonlyAssign'
@@ -188,11 +187,6 @@ grammar =
188
187
  o 'Assignable = INDENT Expression OUTDENT', '["=", 1, 4]'
189
188
  ]
190
189
 
191
- # Rightward assignment (:>) — expression first, target second
192
- RightwardAssign: [
193
- o 'Expression RIGHTWARD_ASSIGN Assignable', '["=", 3, 1]'
194
- ]
195
-
196
190
  # Reactive state (:=) — mutable reactive values
197
191
  ReactiveAssign: [
198
192
  o 'Assignable REACTIVE_ASSIGN Expression' , '["state", 1, 3]'
@@ -899,9 +893,6 @@ grammar =
899
893
  # Postfix existence check: expr? → (expr != null)
900
894
  o 'Value ?' , '["?", 1]'
901
895
 
902
- # Postfix defined check: expr!? → (expr !== undefined)
903
- o 'Value DEFINED' , '["defined", 1]'
904
-
905
896
  # Postfix presence check: expr?! → (expr ? true : undefined) — Houdini operator
906
897
  o 'Value PRESENCE' , '["presence", 1]'
907
898
 
@@ -938,7 +929,6 @@ grammar =
938
929
  o 'Expression && Expression' , '["&&", 1, 3]'
939
930
  o 'Expression || Expression' , '["||", 1, 3]'
940
931
  o 'Expression ?? Expression' , '["??", 1, 3]'
941
- o 'Expression !? Expression' , '["!?", 1, 3]' # Otherwise (undefined-only coalescing)
942
932
 
943
933
  # Pipe
944
934
  o 'Expression PIPE Expression' , '["|>", 1, 3]'
@@ -968,7 +958,7 @@ operators = """
968
958
  right DO_IIFE
969
959
  left . ?.
970
960
  left CALL_START CALL_END
971
- nonassoc ++ -- ? DEFINED PRESENCE
961
+ nonassoc ++ -- ? PRESENCE
972
962
  right UNARY DO
973
963
  right AWAIT
974
964
  right **
@@ -987,7 +977,7 @@ operators = """
987
977
  right TERNARY
988
978
  nonassoc INDENT OUTDENT
989
979
  right YIELD
990
- right = : COMPOUND_ASSIGN RIGHTWARD_ASSIGN RETURN THROW EXTENDS
980
+ right = : COMPOUND_ASSIGN RETURN THROW EXTENDS
991
981
  right FORIN FOROF FORAS FORASAWAIT BY WHEN
992
982
  right IF ELSE FOR WHILE UNTIL LOOP SUPER CLASS COMPONENT RENDER IMPORT EXPORT DYNAMIC_IMPORT OFFER ACCEPT
993
983
  left POST_IF POST_UNLESS
package/src/lexer.js CHANGED
@@ -215,10 +215,9 @@ let UNARY_MATH = new Set(['!', '~']);
215
215
  // Identifier: word chars + optional trailing ! (await) or ? (predicate)
216
216
  // The ? suffix is only captured when NOT followed by . ? ! [ ( to avoid
217
217
  // conflict with ?. (optional chaining), ?? (nullish), ?! (presence), ?.( and ?.[
218
- // The ! suffix is NOT captured when followed by ? to preserve !? as operator
219
- let IDENTIFIER_RE = /^(?!\d)((?:(?!\s)[$\w\x7f-\uffff])+(?:!(?!\?)|[?](?![.?![(]))?)([^\n\S]*:(?![=:>]))?/;
218
+ let IDENTIFIER_RE = /^(?!\d)((?:(?!\s)[$\w\x7f-\uffff])+(?:!|[?](?![.?![(]))?)([^\n\S]*:(?![=:]))?/;
220
219
  let NUMBER_RE = /^0b[01](?:_?[01])*n?|^0o[0-7](?:_?[0-7])*n?|^0x[\da-f](?:_?[\da-f])*n?|^\d+(?:_\d+)*n|^(?:\d+(?:_\d+)*)?\.?\d+(?:_\d+)*(?:e[+-]?\d+(?:_\d+)*)?/i;
221
- let OPERATOR_RE = /^(?:<=>|::|\*>|[-=]>|~>|~=|:>|:=|=!|===|!==|!\?|\?\!|\?\?|=~|\|>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>*\/%])\2=?|\?\.?|\.{2,3})/;
220
+ let OPERATOR_RE = /^(?:<=>|::|\*>|[-=]>|~>|~=|:=|=!|===|!==|\?\!|\?\?|=~|\|>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>*\/%])\2=?|\?\.?|\.{2,3})/;
222
221
  let WHITESPACE_RE = /^[^\n\S]+/;
223
222
  let NEWLINE_RE = /^(?:\n[^\n\S]*)+/;
224
223
  let COMMENT_RE = /^(\s*)###([^#][\s\S]*?)(?:###([^\n\S]*)|###$)|^((?:\s*#(?!##[^#]).*)+)/;
@@ -543,9 +542,12 @@ export class Lexer {
543
542
  }
544
543
 
545
544
  // Reserved words (check the base form, not the suffixed form)
546
- if (tag === 'IDENTIFIER' && RESERVED.has(baseId) &&
547
- !(baseId === 'void' && this.inTypeAnnotation)) {
548
- syntaxError(`reserved word '${baseId}'`, {row: this.row, col: this.col, len: idLen});
545
+ if (tag === 'IDENTIFIER' && RESERVED.has(baseId)) {
546
+ if (baseId === 'void' && (this.inTypeAnnotation || this.prevTag() === '=>')) {
547
+ // ok void used as a type (after :: or =>)
548
+ } else {
549
+ syntaxError(`reserved word '${baseId}'`, {row: this.row, col: this.col, len: idLen});
550
+ }
549
551
  }
550
552
 
551
553
  // Property-specific checks (new.target, import.meta)
@@ -779,15 +781,20 @@ export class Lexer {
779
781
  }
780
782
  }
781
783
  }
782
- // A > that closes a generic type annotation is NOT a continuation
784
+ // A > or >> that closes a generic type annotation/alias is NOT a continuation
783
785
  let prev = this.tokens[this.tokens.length - 1];
784
- if (prev?.[0] === 'COMPARE' && prev[1] === '>') {
786
+ let isGenericClose = (prev?.[0] === 'COMPARE' && prev[1] === '>') ||
787
+ (prev?.[0] === 'SHIFT' && (prev[1] === '>>' || prev[1] === '>>>'));
788
+ if (isGenericClose) {
785
789
  let depth = 0;
786
790
  for (let k = this.tokens.length - 1; k >= 0; k--) {
787
791
  let tk = this.tokens[k];
788
792
  if (tk[0] === 'COMPARE' && tk[1] === '>') depth++;
793
+ else if (tk[0] === 'SHIFT' && tk[1] === '>>') depth += 2;
794
+ else if (tk[0] === 'SHIFT' && tk[1] === '>>>') depth += 3;
789
795
  else if (tk[0] === 'COMPARE' && tk[1] === '<') depth--;
790
796
  if (depth === 0 && tk[0] === 'TYPE_ANNOTATION') return false;
797
+ if (depth === 0 && tk[0] === 'IDENTIFIER' && tk[1] === 'type') return false;
791
798
  if (tk[0] === 'TERMINATOR' || tk[0] === 'INDENT' || tk[0] === 'OUTDENT') break;
792
799
  }
793
800
  }
@@ -1230,7 +1237,6 @@ export class Lexer {
1230
1237
  // Reactive and binding operators
1231
1238
  else if (val === '~=') tag = 'COMPUTED_ASSIGN';
1232
1239
  else if (val === ':=') tag = 'REACTIVE_ASSIGN';
1233
- else if (val === ':>') tag = 'RIGHTWARD_ASSIGN';
1234
1240
  else if (val === '<=>') tag = 'BIND';
1235
1241
  else if (val === '~>') { tag = 'EFFECT'; this.inTypeAnnotation = false; }
1236
1242
  else if (val === '=!') { tag = 'READONLY_ASSIGN'; this.inTypeAnnotation = false; }
@@ -1297,8 +1303,6 @@ export class Lexer {
1297
1303
  else if (SHIFT.has(val)) tag = 'SHIFT';
1298
1304
  // Spaced ? → TERNARY (ternary)
1299
1305
  else if (val === '?' && prev?.spaced) tag = 'TERNARY';
1300
- // Unspaced !? → DEFINED (postfix defined check: v!? → v !== undefined)
1301
- else if (val === '!?' && prev && !prev.spaced) tag = 'DEFINED';
1302
1306
  // Unspaced ?! → PRESENCE (Houdini: v?! → v ? true : undefined)
1303
1307
  else if (val === '?!' && prev && !prev.spaced) tag = 'PRESENCE';
1304
1308
  // ?[ and ?( without dot → treat as optional chaining (?.)