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/CHANGELOG.md +0 -1
- package/README.md +4 -7
- package/bin/rip +16 -4
- package/docs/RIP-LANG.md +0 -42
- package/docs/RIP-TYPES.md +47 -52
- package/docs/demo.html +2 -2
- package/docs/dist/rip.js +1585 -826
- package/docs/dist/rip.min.js +149 -137
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +1 -1
- package/rip-loader.js +2 -2
- package/src/AGENTS.md +1 -2
- package/src/browser.js +5 -5
- package/src/compiler.js +351 -30
- package/src/components.js +176 -11
- package/src/error.js +250 -0
- package/src/grammar/grammar.rip +2 -12
- package/src/lexer.js +15 -11
- package/src/parser.js +220 -223
- package/src/repl.js +3 -2
- package/src/sourcemap-utils.js +39 -6
- package/src/typecheck.js +312 -80
- package/src/types.js +226 -51
- package/src/ui.rip +4 -0
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
|
-
|
|
807
|
-
sl.push(
|
|
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')
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
250
|
+
}
|
package/src/grammar/grammar.rip
CHANGED
|
@@ -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 ++ -- ?
|
|
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
|
|
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
|
-
|
|
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 = /^(?:<=>|::|\*>|[-=]
|
|
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
|
-
|
|
548
|
-
|
|
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
|
-
|
|
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 (?.)
|