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/compiler.js CHANGED
@@ -13,6 +13,7 @@ import { parser } from './parser.js';
13
13
  import { installComponentSupport } from './components.js';
14
14
  import { emitTypes, emitEnum } from './types.js';
15
15
  import { SourceMapGenerator } from './sourcemaps.js';
16
+ import { RipError, toRipError } from './error.js';
16
17
 
17
18
  // =============================================================================
18
19
  // Metadata helpers — isolate all new String() awareness here
@@ -28,7 +29,7 @@ let str = (node) => node instanceof String ? node.valueOf() : node;
28
29
  let INLINE_FORMS = new Set([
29
30
  '+', '-', '*', '/', '%', '//', '%%', '**',
30
31
  '==', '!=', '<', '>', '<=', '>=', '===', '!==',
31
- '&&', '||', '??', '!?', 'not',
32
+ '&&', '||', '??', 'not',
32
33
  '&', '|', '^', '<<', '>>', '>>>',
33
34
  '=', '.', '?.', '[]',
34
35
  '!', 'typeof', 'void', 'delete', 'new',
@@ -149,7 +150,7 @@ export class CodeEmitter {
149
150
  '==': 'emitBinaryOp', '===': 'emitBinaryOp', '!=': 'emitBinaryOp',
150
151
  '!==': 'emitBinaryOp', '<': 'emitBinaryOp', '>': 'emitBinaryOp',
151
152
  '<=': 'emitBinaryOp', '>=': 'emitBinaryOp', '??': 'emitBinaryOp',
152
- '!?': 'emitBinaryOp', '&': 'emitBinaryOp', '|': 'emitBinaryOp',
153
+ '&': 'emitBinaryOp', '|': 'emitBinaryOp',
153
154
  '^': 'emitBinaryOp', '<<': 'emitBinaryOp', '>>': 'emitBinaryOp',
154
155
  '>>>': 'emitBinaryOp',
155
156
 
@@ -212,7 +213,6 @@ export class CodeEmitter {
212
213
  'break': 'emitBreak',
213
214
  'continue': 'emitContinue',
214
215
  '?': 'emitExistential',
215
- 'defined': 'emitDefined',
216
216
  'presence': 'emitPresence',
217
217
  '?:': 'emitTernary',
218
218
  '|>': 'emitPipe',
@@ -277,6 +277,11 @@ export class CodeEmitter {
277
277
  }
278
278
  }
279
279
 
280
+ // Throw a RipError with source location from the nearest s-expression node
281
+ error(message, sexpr, { suggestion } = {}) {
282
+ throw RipError.fromSExpr(message, sexpr, this.options.source, this.options.filename, suggestion);
283
+ }
284
+
280
285
  // ---------------------------------------------------------------------------
281
286
  // Entry point
282
287
  // ---------------------------------------------------------------------------
@@ -371,8 +376,12 @@ export class CodeEmitter {
371
376
  }
372
377
  if (ident) result.push({ name: ident, origLine: node.loc.r, origCol: node.loc.c });
373
378
  }
374
- // Recurse into children (skip head at index 0 — already processed via parent)
375
- for (let i = 1; i < node.length; i++) {
379
+ // Recurse into children (skip head at index 0 — already processed via parent).
380
+ // For arrow functions (-> / =>), skip index 1 (params array) — parameter
381
+ // names are not call expressions and would produce incorrect mappings via
382
+ // the distance heuristic when the same identifier appears in other methods.
383
+ let start = (head === '->' || head === '=>') ? 2 : 1;
384
+ for (let i = start; i < node.length; i++) {
376
385
  if (Array.isArray(node[i])) this.collectSubExprs(node[i], result);
377
386
  }
378
387
  }
@@ -566,7 +575,7 @@ export class CodeEmitter {
566
575
 
567
576
  if (typeof sexpr === 'number') return String(sexpr);
568
577
  if (sexpr === null || sexpr === undefined) return 'null';
569
- if (!Array.isArray(sexpr)) throw new Error(`Invalid s-expression: ${JSON.stringify(sexpr)}`);
578
+ if (!Array.isArray(sexpr)) this.error(`Invalid s-expression: ${JSON.stringify(sexpr)}`, sexpr);
570
579
 
571
580
  let [head, ...rest] = sexpr;
572
581
 
@@ -636,7 +645,7 @@ export class CodeEmitter {
636
645
  return needsAwait ? `await ${callStr}` : callStr;
637
646
  }
638
647
 
639
- throw new Error(`Unknown s-expression type: ${head}`);
648
+ this.error(`Unknown s-expression type: ${head}`, sexpr);
640
649
  }
641
650
 
642
651
  // ---------------------------------------------------------------------------
@@ -654,6 +663,17 @@ export class CodeEmitter {
654
663
  else body.push(stmt);
655
664
  }
656
665
 
666
+ // Classify program-level variables for inline declarations
667
+ let prevInlinePending = this._inlineVarsPending;
668
+ let programInlineVars = new Set();
669
+ if (this.programVars.size > 0 && body.length > 0) {
670
+ let classified = this.classifyVarsForInlining(body, this.programVars);
671
+ programInlineVars = classified.inlineVars;
672
+ // '_' is always emitted as 'var _' (not 'let'), so must not be inlined
673
+ programInlineVars.delete('_');
674
+ if (programInlineVars.size > 0) this._inlineVarsPending = new Set(programInlineVars);
675
+ }
676
+
657
677
  // Generate body first to detect needed helpers
658
678
  let blockStmts = ['def', 'class', 'if', 'for-in', 'for-of', 'for-as', 'while', 'loop', 'switch', 'try'];
659
679
  let stmtEntries = body.map((stmt, index) => {
@@ -680,6 +700,8 @@ export class CodeEmitter {
680
700
  });
681
701
  let statementsCode = stmtEntries.map(e => e.code).join('\n');
682
702
 
703
+ this._inlineVarsPending = prevInlinePending;
704
+
683
705
  let needsBlank = false;
684
706
 
685
707
  if (imports.length > 0) {
@@ -691,10 +713,12 @@ export class CodeEmitter {
691
713
  let hasUnderscore = this.programVars.has('_');
692
714
  if (hasUnderscore) this.programVars.delete('_');
693
715
  if (this.programVars.size > 0) {
694
- let vars = Array.from(this.programVars).sort().join(', ');
695
- if (needsBlank) code += '\n';
696
- code += `let ${vars};\n`;
697
- needsBlank = true;
716
+ let vars = Array.from(this.programVars).filter(v => !programInlineVars.has(v)).sort().join(', ');
717
+ if (vars) {
718
+ if (needsBlank) code += '\n';
719
+ code += `let ${vars};\n`;
720
+ needsBlank = true;
721
+ }
698
722
  }
699
723
  if (hasUnderscore) {
700
724
  if (needsBlank) code += '\n';
@@ -803,10 +827,6 @@ export class CodeEmitter {
803
827
  return `((${a} ${leftOp} ${b}) && (${b} ${op} ${c}))`;
804
828
  }
805
829
  }
806
- if (op === '!?') {
807
- let l = this.emit(left, 'value'), r = this.emit(right, 'value');
808
- return `(${l} !== undefined ? ${l} : ${r})`;
809
- }
810
830
  if (op === '==') op = '===';
811
831
  if (op === '!=') op = '!==';
812
832
  return `(${this.emit(left, 'value')} ${op} ${this.emit(right, 'value')})`;
@@ -860,7 +880,7 @@ export class CodeEmitter {
860
880
  let isFnValue = (this.is(value, '->') || this.is(value, '=>') || this.is(value, 'def'));
861
881
  if (target instanceof String && meta(target, 'await') !== undefined && !isFnValue) {
862
882
  let sigil = meta(target, 'await') === true ? '!' : '&';
863
- throw new Error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'.`);
883
+ this.error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'`, sexpr);
864
884
  }
865
885
 
866
886
  if (target instanceof String && meta(target, 'await') === true && isFnValue) {
@@ -891,6 +911,14 @@ export class CodeEmitter {
891
911
  if (ctrlOp === '||') return `(() => { const __v = ${exprCode}; if (!__v) ${ctrlCode}; return (${targetCode} = __v); })()`;
892
912
  return `(() => { const __v = ${exprCode}; if (__v) ${ctrlCode}; return (${targetCode} = __v); })()`;
893
913
  }
914
+ // Inline `let` for control flow — split into declaration + guard
915
+ let tgtName = (typeof target === 'string') ? target : (target instanceof String) ? str(target) : null;
916
+ if (tgtName && context === 'statement' && this._inlineVarsPending?.delete(tgtName)) {
917
+ let ind = this.indent();
918
+ if (ctrlOp === '??') return `let ${targetCode} = ${exprCode};\n${ind}if (${targetCode} == null) ${ctrlCode}`;
919
+ if (ctrlOp === '||') return `let ${targetCode} = ${exprCode};\n${ind}if (!${targetCode}) ${ctrlCode}`;
920
+ return `let ${targetCode} = ${exprCode};\n${ind}if (${targetCode}) ${ctrlCode}`;
921
+ }
894
922
  if (ctrlOp === '??') return `if ((${targetCode} = ${exprCode}) == null) ${ctrlCode}`;
895
923
  if (ctrlOp === '||') return `if (!(${targetCode} = ${exprCode})) ${ctrlCode}`;
896
924
  return `if ((${targetCode} = ${exprCode})) ${ctrlCode}`;
@@ -934,6 +962,8 @@ export class CodeEmitter {
934
962
  let unwrapped = Array.isArray(wrappedValue) && wrappedValue.length === 1 ? wrappedValue[0] : wrappedValue;
935
963
  let fullValue = [binOp, left, unwrapped];
936
964
  let t = this.emit(target, 'value'), c = this.emit(condition, 'value'), v = this.emit(fullValue, 'value');
965
+ let tgtName = (typeof target === 'string') ? target : (target instanceof String) ? str(target) : null;
966
+ if (tgtName && this._inlineVarsPending?.delete(tgtName)) return `let ${t};\n${this.indent()}if (${c}) ${t} = ${v}`;
937
967
  return `if (${c}) ${t} = ${v}`;
938
968
  }
939
969
  }
@@ -948,6 +978,8 @@ export class CodeEmitter {
948
978
  let t = this.emit(target, 'value');
949
979
  let condCode = this.unwrapLogical(this.emit(condition, 'value'));
950
980
  let v = this.emit(unwrapped, 'value');
981
+ let tgtName = (typeof target === 'string') ? target : (target instanceof String) ? str(target) : null;
982
+ if (tgtName && this._inlineVarsPending?.delete(tgtName)) return `let ${t};\n${this.indent()}if (${condCode}) ${t} = ${v}`;
951
983
  return `if (${condCode}) ${t} = ${v}`;
952
984
  }
953
985
  }
@@ -963,12 +995,23 @@ export class CodeEmitter {
963
995
  }
964
996
 
965
997
  const prevComponentName = this._componentName;
966
- if (this.is(value, 'component') && (typeof target === 'string' || target instanceof String)) this._componentName = str(target);
998
+ const prevComponentTypeParams = this._componentTypeParams;
999
+ if (this.is(value, 'component') && (typeof target === 'string' || target instanceof String)) {
1000
+ this._componentName = str(target);
1001
+ this._componentTypeParams = target.typeParams || '';
1002
+ }
967
1003
  let valueCode = this.emit(value, 'value');
968
1004
  this._componentName = prevComponentName;
1005
+ this._componentTypeParams = prevComponentTypeParams;
969
1006
  let isObjLit = this.is(value, 'object');
970
1007
  if (!isObjLit) valueCode = this.unwrap(valueCode);
971
1008
 
1009
+ // Inline `let` at first assignment for variables classified as inlinable
1010
+ let targetName = (typeof target === 'string') ? target : (target instanceof String) ? str(target) : null;
1011
+ if (head === '=' && targetName && context === 'statement' && this._inlineVarsPending?.delete(targetName)) {
1012
+ return `let ${targetCode} = ${valueCode}`;
1013
+ }
1014
+
972
1015
  let needsParensVal = context === 'value';
973
1016
  let needsParensObj = context === 'statement' && this.is(target, 'object');
974
1017
  if (needsParensVal || needsParensObj) return `(${targetCode} ${op} ${valueCode})`;
@@ -1143,7 +1186,7 @@ export class CodeEmitter {
1143
1186
  if (rest.length === 0) return 'return';
1144
1187
  let [expr] = rest;
1145
1188
  if (this.sideEffectOnly && !(this.is(expr, '->') || this.is(expr, '=>'))) {
1146
- throw new Error(`Cannot return a value from a void function (declared with !)`);
1189
+ this.error('Cannot return a value from a void function (declared with !)', sexpr);
1147
1190
  }
1148
1191
 
1149
1192
  if (this.is(expr, 'if')) {
@@ -1220,10 +1263,6 @@ export class CodeEmitter {
1220
1263
  return `(${this.emit(rest[0], 'value')} != null)`;
1221
1264
  }
1222
1265
 
1223
- emitDefined(head, rest) {
1224
- return `(${this.emit(rest[0], 'value')} !== undefined)`;
1225
- }
1226
-
1227
1266
  emitPresence(head, rest) {
1228
1267
  return `(${this.emit(rest[0], 'value')} ? true : undefined)`;
1229
1268
  }
@@ -1826,7 +1865,7 @@ export class CodeEmitter {
1826
1865
  return switchBody;
1827
1866
  }
1828
1867
 
1829
- emitWhen() { throw new Error('when clause should be handled by switch'); }
1868
+ emitWhen(head, rest, context, sexpr) { this.error('when clause should be handled by switch', sexpr); }
1830
1869
 
1831
1870
  // ---------------------------------------------------------------------------
1832
1871
  // Comprehensions
@@ -2153,9 +2192,14 @@ export class CodeEmitter {
2153
2192
  if (this.options.skipExports) {
2154
2193
  if (this.is(decl, '=')) {
2155
2194
  const prev = this._componentName;
2156
- if (this.is(decl[2], 'component')) this._componentName = str(decl[1]);
2195
+ const prevTP = this._componentTypeParams;
2196
+ if (this.is(decl[2], 'component')) {
2197
+ this._componentName = str(decl[1]);
2198
+ this._componentTypeParams = decl[1]?.typeParams || '';
2199
+ }
2157
2200
  const result = `const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
2158
2201
  this._componentName = prev;
2202
+ this._componentTypeParams = prevTP;
2159
2203
  return result;
2160
2204
  }
2161
2205
  if (Array.isArray(decl) && decl.every(i => typeof i === 'string')) return '';
@@ -2163,9 +2207,14 @@ export class CodeEmitter {
2163
2207
  }
2164
2208
  if (this.is(decl, '=')) {
2165
2209
  const prev = this._componentName;
2166
- if (this.is(decl[2], 'component')) this._componentName = str(decl[1]);
2210
+ const prevTP = this._componentTypeParams;
2211
+ if (this.is(decl[2], 'component')) {
2212
+ this._componentName = str(decl[1]);
2213
+ this._componentTypeParams = decl[1]?.typeParams || '';
2214
+ }
2167
2215
  const result = `export const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
2168
2216
  this._componentName = prev;
2217
+ this._componentTypeParams = prevTP;
2169
2218
  return result;
2170
2219
  }
2171
2220
  if (Array.isArray(decl) && decl.every(i => typeof i === 'string')) return `export { ${decl.join(', ')} }`;
@@ -2393,9 +2442,19 @@ export class CodeEmitter {
2393
2442
  this.restMiddleParam = null;
2394
2443
  }
2395
2444
 
2445
+ // Classify variables: inline `let` at first assignment vs hoist to top
2446
+ let prevInlinePending = this._inlineVarsPending;
2447
+ let inlineVars = new Set();
2448
+ if (newVars.size > 0 && statements.length > 0) {
2449
+ let classified = this.classifyVarsForInlining(statements, newVars);
2450
+ inlineVars = classified.inlineVars;
2451
+ if (inlineVars.size > 0) this._inlineVarsPending = new Set(inlineVars);
2452
+ }
2453
+
2396
2454
  this.indentLevel++;
2397
2455
  let code = '{\n';
2398
- if (newVars.size > 0) code += this.indent() + `let ${Array.from(newVars).sort().join(', ')};\n`;
2456
+ let hoistVars = new Set([...newVars].filter(v => !inlineVars.has(v)));
2457
+ if (hoistVars.size > 0) code += this.indent() + `let ${Array.from(hoistVars).sort().join(', ')};\n`;
2399
2458
 
2400
2459
  let firstIsSuper = autoAssignments.length > 0 && statements.length > 0 &&
2401
2460
  Array.isArray(statements[0]) && statements[0][0] === 'super';
@@ -2426,6 +2485,8 @@ export class CodeEmitter {
2426
2485
  if (typeof target === 'string' && Array.isArray(value)) {
2427
2486
  let vh = value[0];
2428
2487
  if (vh === 'comprehension' || vh === 'for-in') {
2488
+ // Comprehension targets with inline vars: declare before the loop
2489
+ if (this._inlineVarsPending?.delete(target)) code += this.indent() + `let ${target};\n`;
2429
2490
  this.comprehensionTarget = target;
2430
2491
  code += this.emit(value, 'value');
2431
2492
  this.comprehensionTarget = null;
@@ -2433,6 +2494,14 @@ export class CodeEmitter {
2433
2494
  return;
2434
2495
  }
2435
2496
  }
2497
+ // Handle inline vars at last statement (auto-return): split into let + return
2498
+ if ((typeof target === 'string' || target instanceof String) && this._inlineVarsPending?.has(str(target))) {
2499
+ this._inlineVarsPending.delete(str(target));
2500
+ let assignCode = this.emit(stmt, 'statement');
2501
+ code += this.indent() + 'let ' + this.addSemicolon(stmt, assignCode) + '\n';
2502
+ code += this.indent() + `return ${str(target)};\n`;
2503
+ return;
2504
+ }
2436
2505
  }
2437
2506
 
2438
2507
  let needsReturn = !isConstructor && !sideEffectOnly && isLast &&
@@ -2461,6 +2530,7 @@ export class CodeEmitter {
2461
2530
  if (!noRetStmts.includes(lastH)) code += this.indent() + 'return;\n';
2462
2531
  }
2463
2532
 
2533
+ this._inlineVarsPending = prevInlinePending;
2464
2534
  this.indentLevel--;
2465
2535
  code += this.indent() + '}';
2466
2536
  this.scopeStack.pop();
@@ -3093,6 +3163,197 @@ export class CodeEmitter {
3093
3163
  return false;
3094
3164
  }
3095
3165
 
3166
+ // ---------------------------------------------------------------------------
3167
+ // Variable inlining — emit `let` at first assignment instead of hoisting
3168
+ // ---------------------------------------------------------------------------
3169
+
3170
+ // Check if an s-expression references a variable name (stopping at function boundaries)
3171
+ referencesVar(sexpr, varName) {
3172
+ if (!sexpr) return false;
3173
+ if (sexpr instanceof String) return str(sexpr) === varName;
3174
+ if (typeof sexpr === 'string') return sexpr === varName;
3175
+ if (!Array.isArray(sexpr)) return false;
3176
+ let h = sexpr[0];
3177
+ let hs = (typeof h === 'string') ? h : (h instanceof String) ? str(h) : null;
3178
+ if (hs === 'def' || hs === '->' || hs === '=>' || hs === 'effect') return false;
3179
+ // Property access: only check the object, not the property name
3180
+ if (hs === '.' || hs === '?.') return this.referencesVar(sexpr[1], varName);
3181
+ // Object literal: check values but not simple string keys
3182
+ if (hs === 'object') {
3183
+ for (let i = 1; i < sexpr.length; i++) {
3184
+ let pair = sexpr[i];
3185
+ if (Array.isArray(pair)) {
3186
+ if (this.is(pair, '...')) { if (this.referencesVar(pair[1], varName)) return true; }
3187
+ else if (pair.length >= 2) { if (this.referencesVar(pair[pair.length - 1], varName)) return true; }
3188
+ } else { if (this.referencesVar(pair, varName)) return true; }
3189
+ }
3190
+ return false;
3191
+ }
3192
+ return sexpr.some(item => this.referencesVar(item, varName));
3193
+ }
3194
+
3195
+ // Check if the first reference to a variable in DFS order is a `=` assignment
3196
+ // at statement level (not inside a value expression like another assignment's RHS).
3197
+ // Returns true only when it's safe to emit `let` at the first assignment site.
3198
+ firstRefIsAssignment(sexpr, varName) {
3199
+ let result = null; // null = not found, 'write' = statement-level assignment, 'read' = read/value
3200
+ let isVar = (n) => (n instanceof String ? str(n) : n) === varName;
3201
+ let walk = (node, inValue) => {
3202
+ if (result !== null) return;
3203
+ if (!node) return;
3204
+ if (!Array.isArray(node)) { if (isVar(node)) result = 'read'; return; }
3205
+ let h = node[0];
3206
+ let hs = (typeof h === 'string') ? h : (h instanceof String) ? str(h) : null;
3207
+ if (hs === 'def' || hs === '->' || hs === '=>' || hs === 'effect') return;
3208
+ // Assignment to our variable
3209
+ if (hs === '=' && (typeof node[1] === 'string' || node[1] instanceof String) && str(node[1]) === varName) {
3210
+ if (inValue) { result = 'read'; return; } // In value context — can't emit `let` here
3211
+ result = this.referencesVar(node[2], varName) ? 'read' : 'write';
3212
+ return;
3213
+ }
3214
+ // Compound assignment reads the variable
3215
+ if (CodeEmitter.ASSIGNMENT_OPS.has(hs) && hs !== '=' &&
3216
+ (typeof node[1] === 'string' || node[1] instanceof String) && str(node[1]) === varName) {
3217
+ result = 'read'; return;
3218
+ }
3219
+ // Property access: only check object
3220
+ if (hs === '.' || hs === '?.') { walk(node[1], inValue); return; }
3221
+ // Object literal: skip simple string keys
3222
+ if (hs === 'object') {
3223
+ for (let i = 1; i < node.length; i++) {
3224
+ let pair = node[i];
3225
+ if (Array.isArray(pair)) {
3226
+ if (this.is(pair, '...')) walk(pair[1], true);
3227
+ else if (pair.length >= 2) walk(pair[pair.length - 1], true);
3228
+ } else walk(pair, true);
3229
+ }
3230
+ return;
3231
+ }
3232
+ // Block: children are at statement level
3233
+ if (hs === 'block') { for (let i = 1; i < node.length; i++) walk(node[i], false); return; }
3234
+ // If: condition is value, branches maintain parent context
3235
+ if (hs === 'if') { walk(node[1], true); for (let i = 2; i < node.length; i++) walk(node[i], inValue); return; }
3236
+ // Loops: iterable/condition is value, body is statement
3237
+ if (hs === 'for-in' || hs === 'for-of' || hs === 'for-as') { walk(node[2], true); if (node.length > 3) walk(node[node.length - 1], false); return; }
3238
+ if (hs === 'while') { walk(node[1], true); walk(node[2], false); return; }
3239
+ // Try: blocks are statement level
3240
+ if (hs === 'try') { for (let i = 1; i < node.length; i++) walk(node[i], false); return; }
3241
+ // Other assignments: RHS is value context
3242
+ if (CodeEmitter.ASSIGNMENT_OPS.has(hs)) { walk(node[2], true); return; }
3243
+ // Default: everything else is value context
3244
+ for (let i = 0; i < node.length; i++) walk(node[i], true);
3245
+ };
3246
+ walk(sexpr, false); // top of a statement is statement context
3247
+ return result === 'write';
3248
+ }
3249
+
3250
+ // Check if all references to a variable within a statement are contained in a
3251
+ // single block child. Returns false if the variable appears at multiple nesting
3252
+ // levels (e.g., inside an if-branch AND at the same level as the if).
3253
+ allRefsInSingleBlock(sexpr, varName) {
3254
+ if (!Array.isArray(sexpr)) return false;
3255
+ let h = sexpr[0];
3256
+ let hs = (typeof h === 'string') ? h : (h instanceof String) ? str(h) : null;
3257
+ // For 'if': check condition + each branch. Variable must appear in exactly one branch,
3258
+ // and NOT in the condition. Then recurse into that branch.
3259
+ if (hs === 'if') {
3260
+ if (this.referencesVar(sexpr[1], varName)) return false; // in condition
3261
+ let branchCount = 0;
3262
+ let refBranch = null;
3263
+ for (let i = 2; i < sexpr.length; i++) {
3264
+ if (this.referencesVar(sexpr[i], varName)) { branchCount++; refBranch = sexpr[i]; }
3265
+ }
3266
+ if (branchCount !== 1) return false;
3267
+ return this.allRefsInSingleBlock(refBranch, varName);
3268
+ }
3269
+ // For 'block': variable must appear in only one child statement.
3270
+ // Then recurse into that child.
3271
+ if (hs === 'block') {
3272
+ let childCount = 0;
3273
+ let refChild = null;
3274
+ for (let i = 1; i < sexpr.length; i++) {
3275
+ if (this.referencesVar(sexpr[i], varName)) { childCount++; refChild = sexpr[i]; }
3276
+ }
3277
+ if (childCount !== 1) return false;
3278
+ // If the single child IS a direct assignment to this variable, we've bottomed out — safe.
3279
+ if (Array.isArray(refChild) && refChild[0] === '=' &&
3280
+ (typeof refChild[1] === 'string' || refChild[1] instanceof String) &&
3281
+ str(refChild[1]) === varName) return true;
3282
+ return this.allRefsInSingleBlock(refChild, varName);
3283
+ }
3284
+ // For loops: iterable/condition must not reference var, body is a block — recurse.
3285
+ if (hs === 'for-in' || hs === 'for-of' || hs === 'for-as') {
3286
+ if (this.referencesVar(sexpr[2], varName)) return false;
3287
+ return true;
3288
+ }
3289
+ if (hs === 'while') {
3290
+ if (this.referencesVar(sexpr[1], varName)) return false;
3291
+ return true;
3292
+ }
3293
+ // For try: recurse
3294
+ if (hs === 'try') return true;
3295
+ return false;
3296
+ }
3297
+
3298
+ // Classify variables into those that can have `let` inlined at their first assignment
3299
+ // vs those that must be hoisted to the function/program top.
3300
+ classifyVarsForInlining(statements, vars) {
3301
+ if (vars.size === 0) return { inlineVars: new Set(), hoistVars: new Set(vars) };
3302
+
3303
+ // Pre-scan: variables with type annotations (::) must be hoisted. The DTS header
3304
+ // declares them as `let x: Type;` — inlining `let x = value;` in the body would
3305
+ // create a duplicate declaration, causing TS2454 ("used before being assigned").
3306
+ let typedVars = new Set();
3307
+ let findTypedAssigns = (node) => {
3308
+ if (!Array.isArray(node)) return;
3309
+ if (node[0] === '=' && node[1] instanceof String && node[1].type && vars.has(str(node[1]))) {
3310
+ typedVars.add(str(node[1]));
3311
+ }
3312
+ for (let i = 1; i < node.length; i++) findTypedAssigns(node[i]);
3313
+ };
3314
+ for (let stmt of statements) findTypedAssigns(stmt);
3315
+
3316
+ // For each variable, find which statement indices reference it
3317
+ let varStmts = new Map();
3318
+ for (let v of vars) varStmts.set(v, []);
3319
+ for (let i = 0; i < statements.length; i++) {
3320
+ for (let v of vars) {
3321
+ if (this.referencesVar(statements[i], v)) varStmts.get(v).push(i);
3322
+ }
3323
+ }
3324
+ let inlineVars = new Set(), hoistVars = new Set();
3325
+ for (let [v, indices] of varStmts) {
3326
+ if (indices.length === 0) { hoistVars.add(v); continue; }
3327
+ // Typed variables must be hoisted — DTS header already declares them
3328
+ if (typedVars.has(v)) { hoistVars.add(v); continue; }
3329
+ let firstIdx = indices[0];
3330
+ let firstStmt = statements[firstIdx];
3331
+ // Check if the first statement containing this variable is a direct `=` assignment to it
3332
+ let isDirectAssign = Array.isArray(firstStmt) && firstStmt[0] === '=' &&
3333
+ (typeof firstStmt[1] === 'string' || firstStmt[1] instanceof String) &&
3334
+ str(firstStmt[1]) === v &&
3335
+ !this.referencesVar(firstStmt[2], v);
3336
+ if (isDirectAssign) {
3337
+ // First reference is a direct body-level assignment — safe to inline `let` here
3338
+ // (same scope as hoisted `let`, but TS can infer the type from the initializer)
3339
+ inlineVars.add(v);
3340
+ } else if (indices.length === 1 && !this.is(firstStmt, 'switch') &&
3341
+ this.allRefsInSingleBlock(firstStmt, v) &&
3342
+ this.firstRefIsAssignment(firstStmt, v)) {
3343
+ // All references are within a single block branch of a non-switch statement
3344
+ // (e.g., all inside one if-branch, one for body, etc.) and the first DFS
3345
+ // reference is a direct assignment. JS 'let' block scoping is safe here
3346
+ // because the variable never escapes that single block.
3347
+ inlineVars.add(v);
3348
+ } else {
3349
+ // Variable spans multiple statements, is in a switch, or first ref is a read.
3350
+ // Must hoist to ensure correct scoping.
3351
+ hoistVars.add(v);
3352
+ }
3353
+ }
3354
+ return { inlineVars, hoistVars };
3355
+ }
3356
+
3096
3357
  // Class helpers
3097
3358
  extractMemberName(mk) {
3098
3359
  if (this.is(mk, '.') && mk[1] === 'this') return mk[2];
@@ -3362,7 +3623,12 @@ export class Compiler {
3362
3623
 
3363
3624
  // Step 1: Tokenize (includes rewriteTypes() via installTypeSupport)
3364
3625
  let lexer = new Lexer();
3365
- let tokens = lexer.tokenize(source);
3626
+ let tokens;
3627
+ try {
3628
+ tokens = lexer.tokenize(source);
3629
+ } catch (err) {
3630
+ throw toRipError(err, source, this.options.filename);
3631
+ }
3366
3632
  if (this.options.showTokens) {
3367
3633
  tokens.forEach(t => console.log(`${t[0].padEnd(12)} ${JSON.stringify(t[1])}`));
3368
3634
  console.log();
@@ -3378,6 +3644,46 @@ export class Compiler {
3378
3644
  // Remove TYPE_DECL markers — the parser doesn't know about them
3379
3645
  tokens = tokens.filter(t => t[0] !== 'TYPE_DECL');
3380
3646
 
3647
+ // Elide type-only imports — after type stripping, imported names that were
3648
+ // only used in type annotations no longer appear in the token stream.
3649
+ // Only elide when at least one name was consumed by type annotation stripping.
3650
+ if (lexer.typeRefNames?.size > 0) {
3651
+ let typeRefNames = lexer.typeRefNames;
3652
+ let usedNames = new Set();
3653
+ let inImport = false;
3654
+ for (let t of tokens) {
3655
+ if (t[0] === 'IMPORT') { inImport = true; continue; }
3656
+ if (inImport && t[0] === 'TERMINATOR') { inImport = false; continue; }
3657
+ if (inImport) continue;
3658
+ if (t[0] === 'IDENTIFIER') usedNames.add(t[1]);
3659
+ }
3660
+ for (let i = tokens.length - 1; i >= 0; i--) {
3661
+ if (tokens[i][0] !== 'IMPORT') continue;
3662
+ let j = i + 1;
3663
+ if (j >= tokens.length) continue;
3664
+ // Skip dynamic imports: import(expr)
3665
+ if (tokens[j][0] === 'CALL_START' || tokens[j][0] === '(') continue;
3666
+ // Skip side-effect imports: import 'module'
3667
+ if (tokens[j][0] === 'STRING') continue;
3668
+ // Collect imported names between IMPORT and FROM
3669
+ let names = [];
3670
+ while (j < tokens.length && tokens[j][0] !== 'FROM' && tokens[j][0] !== 'TERMINATOR') {
3671
+ if (tokens[j][0] === 'IDENTIFIER') names.push(tokens[j][1]);
3672
+ j++;
3673
+ }
3674
+ if (names.length === 0) continue;
3675
+ // Keep if any name is used at runtime
3676
+ if (names.some(n => usedNames.has(n))) continue;
3677
+ // Only elide if at least one name was used in a type annotation
3678
+ if (!names.some(n => typeRefNames.has(n))) continue;
3679
+ // All imported names are type-only — remove IMPORT through TERMINATOR
3680
+ let end = j;
3681
+ while (end < tokens.length && tokens[end][0] !== 'TERMINATOR') end++;
3682
+ if (end < tokens.length) end++; // include TERMINATOR
3683
+ tokens.splice(i, end - i);
3684
+ }
3685
+ }
3686
+
3381
3687
  // Strip leading terminators that may result from removed type declarations
3382
3688
  while (tokens.length > 0 && tokens[0][0] === 'TERMINATOR') {
3383
3689
  tokens.shift();
@@ -3390,6 +3696,7 @@ export class Compiler {
3390
3696
  }
3391
3697
 
3392
3698
  // Step 3: Parse — shim adapter wraps token values with metadata
3699
+ let lastLexedLoc = null;
3393
3700
  parser.lexer = {
3394
3701
  tokens, pos: 0,
3395
3702
  setInput: function() {},
@@ -3404,6 +3711,8 @@ export class Compiler {
3404
3711
  }
3405
3712
  this.text = val;
3406
3713
  this.loc = token.loc;
3714
+ this.line = token.loc?.r;
3715
+ lastLexedLoc = token.loc;
3407
3716
  return token[0];
3408
3717
  }
3409
3718
  };
@@ -3411,11 +3720,21 @@ export class Compiler {
3411
3720
  let sexpr;
3412
3721
  try {
3413
3722
  sexpr = parser.parse(source);
3414
- } catch (parseError) {
3723
+ } catch (err) {
3415
3724
  if (/\?\s*\([^)]*\?[^)]*:[^)]*\)\s*:/.test(source) || /\?\s+\w+\s+\?\s+/.test(source)) {
3416
- throw new Error('Nested ternary operators are not supported. Use if/else statements instead.');
3725
+ throw new RipError('Nested ternary operators are not supported', {
3726
+ code: 'E_PARSE', source, file: this.options.filename,
3727
+ suggestion: 'Use if/else statements instead.',
3728
+ phase: 'parser',
3729
+ });
3730
+ }
3731
+ let re = toRipError(err, source, this.options.filename);
3732
+ if (re.phase === 'parser' && lastLexedLoc) {
3733
+ re.line = lastLexedLoc.r ?? re.line;
3734
+ re.column = lastLexedLoc.c ?? re.column;
3735
+ re.length = lastLexedLoc.n || 1;
3417
3736
  }
3418
- throw parseError;
3737
+ throw re;
3419
3738
  }
3420
3739
 
3421
3740
  if (this.options.showSExpr) {
@@ -3434,6 +3753,7 @@ export class Compiler {
3434
3753
  let generator = new CodeEmitter({
3435
3754
  dataSection,
3436
3755
  source,
3756
+ filename: this.options.filename,
3437
3757
  skipPreamble: this.options.skipPreamble,
3438
3758
  skipRuntimes: this.options.skipRuntimes,
3439
3759
  skipExports: this.options.skipExports,
@@ -3521,3 +3841,4 @@ export function getComponentRuntime() {
3521
3841
  }
3522
3842
 
3523
3843
  export { formatSExpr };
3844
+ export { RipError, toRipError, formatError, formatErrorHTML } from './error.js';