rip-lang 3.13.134 → 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/components.js CHANGED
@@ -724,7 +724,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
724
724
  } else if (op === 'computed') {
725
725
  const varName = getMemberName(stmt[1]);
726
726
  if (varName) {
727
- derivedVars.push({ name: varName, expr: stmt[2] });
727
+ derivedVars.push({ name: varName, expr: stmt[2], type: getMemberType(stmt[1]) });
728
728
  memberNames.add(varName);
729
729
  reactiveMembers.add(varName);
730
730
  }
@@ -803,8 +803,9 @@ export function installComponentSupport(CodeEmitter, Lexer) {
803
803
  .replace(/(\w+(?:<[^>]+>)?)\!/g, 'NonNullable<$1>') : null;
804
804
 
805
805
  const sl = [];
806
- sl.push('class {');
807
- sl.push(' declare _root: Element | null;');
806
+ const componentTypeParams = this._componentTypeParams || '';
807
+ sl.push(`class ${componentTypeParams}{`);
808
+ sl.push(' declare _root: Element | null; declare app: any;');
808
809
  sl.push(' emit(_name: string, _detail?: any): void {}');
809
810
 
810
811
  // Constructor — typed props for public state/readonly (matches DTS)
@@ -849,14 +850,16 @@ export function installComponentSupport(CodeEmitter, Lexer) {
849
850
  const ts = expandType(type) || inferLiteralType(value);
850
851
  sl.push(ts ? ` declare ${name}: ${ts};` : ` declare ${name}: any;`);
851
852
  }
852
- for (const { name, expr } of derivedVars) {
853
+ for (const { name, expr, type } of derivedVars) {
854
+ const ts = expandType(type);
855
+ const typeAnnot = ts ? `: Computed<${ts}>` : '';
853
856
  if (this.is(expr, 'block')) {
854
857
  const transformed = this.transformComponentMembers(expr);
855
858
  const body = this.emitFunctionBody(transformed);
856
- sl.push(` ${name} = __computed(() => ${body});`);
859
+ sl.push(` ${name}${typeAnnot} = __computed(() => ${body});`);
857
860
  } else {
858
861
  const val = this.emitInComponent(expr, 'value');
859
- sl.push(` ${name} = __computed(() => ${val});`);
862
+ sl.push(` ${name}${typeAnnot} = __computed(() => ${val});`);
860
863
  }
861
864
  }
862
865
 
@@ -975,6 +978,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
975
978
  if (renderBlock) {
976
979
  const constructions = [];
977
980
  let constructionIdx = 0;
981
+ const sourceLines = this.options.source?.split('\n');
978
982
  const extractProps = (args) => {
979
983
  const props = [];
980
984
  for (const arg of args) {
@@ -1031,7 +1035,14 @@ export function installComponentSupport(CodeEmitter, Lexer) {
1031
1035
  const val = this.emitInComponent(value, 'value');
1032
1036
  props.push({ code: `'${eventKey}': ${val}`, srcLine });
1033
1037
  } else if (typeof key === 'string') {
1034
- if (key === 'key') continue;
1038
+ if (key === 'key') {
1039
+ // key: is not an HTML attribute, but emit its value
1040
+ // expression for type-checking and semantic tokens
1041
+ const val = this.emitInComponent(value, 'value');
1042
+ const marker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
1043
+ constructions.push(` (${val});${marker}`);
1044
+ continue;
1045
+ }
1035
1046
  if (key.startsWith('__bind_') && key.endsWith('__')) {
1036
1047
  const propName = key.slice(7, -2);
1037
1048
  const val = this.emitInComponent(value, 'value');
@@ -1049,6 +1060,146 @@ export function installComponentSupport(CodeEmitter, Lexer) {
1049
1060
  const walkRender = (node) => {
1050
1061
  if (!Array.isArray(node)) return;
1051
1062
  const head = node[0]?.valueOf?.() ?? node[0];
1063
+
1064
+ // Object nodes are property bags (key-value pairs) — their values
1065
+ // are code expressions (event handlers, bindings, literals), not
1066
+ // render template. extractIntrinsicProps handles them separately.
1067
+ // Walking into them would treat function bodies as template content
1068
+ // (e.g. `@blur: (e) -> p(e)` would emit `e;` and `__ripEl('p')`).
1069
+ if (head === 'object') return;
1070
+
1071
+ // Type-check conditional and loop expressions in render blocks.
1072
+ // Without this, `if labelz` (a typo for `label`) silently evaluates
1073
+ // as undefined and skips the block — the condition goes unchecked.
1074
+ // Similarly, `switch statusz` and `for item in itemsz` go unchecked.
1075
+ if (head === 'if' || head === 'unless') {
1076
+ const condition = node[1];
1077
+ if (condition != null) {
1078
+ const condCode = this.emitInComponent(condition, 'value');
1079
+ const srcLine = node.loc?.r;
1080
+ const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
1081
+ constructions.push(` ${condCode};${srcMarker}`);
1082
+ }
1083
+ } else if (head === '?:') {
1084
+ // Emit the full ternary so all branches are type-checked
1085
+ const ternCode = this.emitInComponent(node, 'value');
1086
+ const srcLine = node.loc?.r;
1087
+ const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
1088
+ constructions.push(` ${ternCode};${srcMarker}`);
1089
+ } else if (head === 'switch') {
1090
+ const discriminant = node[1];
1091
+ if (discriminant != null) {
1092
+ const discCode = this.emitInComponent(discriminant, 'value');
1093
+ const srcLine = node.loc?.r;
1094
+ const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
1095
+ constructions.push(` ${discCode};${srcMarker}`);
1096
+ }
1097
+ } else if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
1098
+ // Emit a real for-loop so the loop variable is in scope for the body.
1099
+ // node: [head, vars, iterable, step, guard, body]
1100
+ const vars = node[1];
1101
+ const iterable = node[2];
1102
+ if (iterable != null) {
1103
+ const iterCode = this.emitInComponent(iterable, 'value');
1104
+ const srcLine = node.loc?.r;
1105
+ const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
1106
+ // Extract loop variable pattern
1107
+ let varPattern;
1108
+ if (Array.isArray(vars)) {
1109
+ if (vars.length === 1) {
1110
+ const v = vars[0];
1111
+ varPattern = Array.isArray(v) ? this.emitDestructuringPattern(v) : String(v);
1112
+ } else if (head === 'for-of') {
1113
+ // for key, val of obj — destructure as [key, val] from Object.entries
1114
+ varPattern = `[${vars.map(v => String(v)).join(', ')}]`;
1115
+ } else {
1116
+ // for item, index in arr — first is the item
1117
+ varPattern = String(vars[0]);
1118
+ }
1119
+ } else {
1120
+ varPattern = String(vars);
1121
+ }
1122
+ if (head === 'for-of') {
1123
+ constructions.push(` for (const ${varPattern} of Object.entries(${iterCode})) {${srcMarker}`);
1124
+ } else {
1125
+ constructions.push(` for (const ${varPattern} of ${iterCode}) {${srcMarker}`);
1126
+ }
1127
+ // Walk body children (indices 3+ may contain guard, body, etc.)
1128
+ for (let bi = 3; bi < node.length; bi++) {
1129
+ if (node[bi] != null) walkRender(node[bi]);
1130
+ }
1131
+ constructions.push(` }`);
1132
+ return; // Don't walk children again below
1133
+ }
1134
+ } else if (head === '__text__') {
1135
+ // = expr — text expression: emit the expression for type-checking
1136
+ const textExpr = node[1];
1137
+ if (textExpr != null) {
1138
+ const exprCode = this.emitInComponent(textExpr, 'value');
1139
+ const srcLine = node.loc?.r;
1140
+ const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
1141
+ constructions.push(` ${exprCode};${srcMarker}`);
1142
+ }
1143
+ }
1144
+
1145
+ // Emit a bare lowercase identifier as either a property access
1146
+ // (component member used as text), __ripEl (tag name check when at
1147
+ // block level), or a plain variable reference (text child of a tag).
1148
+ const emitBareIdent = (child, parentNode, isTextChild) => {
1149
+ if (typeof child !== 'string' || !/^[a-z][\w-]*$/.test(child)) return;
1150
+ if (CodeEmitter.GENERATORS[child]) return;
1151
+ if (child === 'null' || child === 'undefined' || child === 'true' || child === 'false') return;
1152
+ let srcLine = parentNode.loc?.r;
1153
+ if (srcLine != null && sourceLines) {
1154
+ const re = new RegExp(`\\b${child}\\b`);
1155
+ for (let ln = srcLine; ln < sourceLines.length; ln++) {
1156
+ if (re.test(sourceLines[ln])) { srcLine = ln; break; }
1157
+ }
1158
+ }
1159
+ const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
1160
+ if (this.componentMembers && this.componentMembers.has(child)) {
1161
+ constructions.push(` this.${child};${srcMarker}`);
1162
+ } else if (isTextChild) {
1163
+ // Text child of a tag — emit as variable reference so TS
1164
+ // reports "Cannot find name 'x'" instead of "not a known element"
1165
+ constructions.push(` ${child};${srcMarker}`);
1166
+ } else {
1167
+ constructions.push(` __ripEl('${child}');${srcMarker}`);
1168
+ }
1169
+ };
1170
+
1171
+ // Bare lowercase identifiers inside a block or as children of tag nodes
1172
+ // — emit __ripEl so TS catches tag typos (e.g., slotz for slot), or
1173
+ // emit this.prop for component member text references.
1174
+ const isTagHead = typeof head === 'string' && /^[a-z][\w-]*$/.test(head) &&
1175
+ !CodeEmitter.GENERATORS[head] && TEMPLATE_TAGS.has(head.split(/[.#]/)[0]);
1176
+ if (head === 'block') {
1177
+ for (let i = 1; i < node.length; i++) emitBareIdent(node[i], node, false);
1178
+ } else if (isTagHead) {
1179
+ for (let i = 1; i < node.length; i++) emitBareIdent(node[i], node, true);
1180
+ // Emit expression children of intrinsic tags for type-checking.
1181
+ // Without this, text content like "#{item.name}" in `li "#{item.name}"`
1182
+ // is invisible to TypeScript and loop variables appear unused (TS 6133).
1183
+ for (let i = 1; i < node.length; i++) {
1184
+ const child = node[i];
1185
+ if (!Array.isArray(child)) continue;
1186
+ const ch = child[0]?.valueOf?.() ?? child[0];
1187
+ if (ch === 'object' || ch === 'block' || ch === '__text__') continue;
1188
+ if (typeof ch === 'string') {
1189
+ if (/^[A-Z]/.test(ch)) continue;
1190
+ if (TEMPLATE_TAGS.has(ch.split(/[.#]/)[0])) continue;
1191
+ if (/^[a-z][\w-]*$/.test(ch) && !CodeEmitter.GENERATORS[ch]) continue;
1192
+ if (/^(if|unless|switch|for-in|for-of|for-as|while|until|loop|loop-n|try|throw|break|continue|break-if|continue-if|control|when|return|def|->|=>|class|enum|state|computed|readonly|effect|=|program)$/.test(ch)) continue;
1193
+ }
1194
+ try {
1195
+ const exprCode = this.emitInComponent(child, 'value');
1196
+ const srcLine = child.loc?.r ?? node.loc?.r;
1197
+ const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
1198
+ constructions.push(` ${exprCode};${srcMarker}`);
1199
+ } catch {}
1200
+ }
1201
+ }
1202
+ for (let i = 1; i < node.length; i++) walkRender(node[i]);
1052
1203
  if (typeof head === 'string' && /^[A-Z]/.test(head)) {
1053
1204
  const props = extractProps(node.slice(1));
1054
1205
  const varName = `_${constructionIdx++}`;
@@ -1100,7 +1251,6 @@ export function installComponentSupport(CodeEmitter, Lexer) {
1100
1251
  }
1101
1252
  }
1102
1253
  }
1103
- for (let i = 1; i < node.length; i++) walkRender(node[i]);
1104
1254
  };
1105
1255
  walkRender(renderBlock);
1106
1256
  if (constructions.length > 0) {
@@ -1346,15 +1496,15 @@ export function installComponentSupport(CodeEmitter, Lexer) {
1346
1496
  * Handle standalone render (outside component): error
1347
1497
  */
1348
1498
  proto.emitRender = function(head, rest, context, sexpr) {
1349
- throw new Error('render blocks can only be used inside a component');
1499
+ this.error('render blocks can only be used inside a component', sexpr);
1350
1500
  };
1351
1501
 
1352
1502
  proto.emitOffer = function(head, rest, context, sexpr) {
1353
- throw new Error('offer can only be used inside a component');
1503
+ this.error('offer can only be used inside a component', sexpr);
1354
1504
  };
1355
1505
 
1356
1506
  proto.emitAccept = function(head, rest, context, sexpr) {
1357
- throw new Error('accept can only be used inside a component');
1507
+ this.error('accept can only be used inside a component', sexpr);
1358
1508
  };
1359
1509
 
1360
1510
  // ==========================================================================
@@ -1998,6 +2148,18 @@ export function installComponentSupport(CodeEmitter, Lexer) {
1998
2148
 
1999
2149
  proto.emitConditional = function(sexpr) {
2000
2150
  this._pendingAutoWire = false;
2151
+
2152
+ // Fold flat else-if chains into nested structure.
2153
+ // Parser emits: ['if', c1, t1, ['if', c2, t2], ..., finalElse]
2154
+ // We need: ['if', c1, t1, ['if', c2, t2, [..., finalElse]]]
2155
+ if (sexpr.length > 4) {
2156
+ let chain = sexpr[sexpr.length - 1];
2157
+ for (let i = sexpr.length - 2; i >= 3; i--) {
2158
+ chain = [...sexpr[i], chain];
2159
+ }
2160
+ sexpr = [sexpr[0], sexpr[1], sexpr[2], chain];
2161
+ }
2162
+
2001
2163
  const [, condition, thenBlock, elseBlock] = sexpr;
2002
2164
 
2003
2165
  const anchorVar = this.newElementVar('anchor');
@@ -2055,6 +2217,9 @@ export function installComponentSupport(CodeEmitter, Lexer) {
2055
2217
  setupLines.push(` }`);
2056
2218
  }
2057
2219
  setupLines.push(` ${effClose}`);
2220
+ if (this._factoryMode) {
2221
+ setupLines.push(` disposers.push(() => { if (currentBlock) { currentBlock.d(true); currentBlock = null; } });`);
2222
+ }
2058
2223
  setupLines.push(`}`);
2059
2224
 
2060
2225
  this._setupLines.push(setupLines.join('\n '));
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 (?.)