rip-lang 3.16.0 → 3.16.1

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
@@ -322,6 +322,20 @@ export class CodeEmitter {
322
322
  // Each entry pairs a statement's generated code with its source loc.
323
323
  // Output line positions are computed by exact arithmetic — no heuristics.
324
324
  buildMappings() {
325
+ // Imports are emitted at the top of the file (before the preamble),
326
+ // starting at line 0. Process them first with their own line offset.
327
+ if (this._importEntries) {
328
+ let importLineOffset = 0;
329
+ for (let entry of this._importEntries) {
330
+ if (entry.loc) {
331
+ this.sourceMap.addMapping(importLineOffset, 0, entry.loc.r, entry.loc.c);
332
+ }
333
+ if (entry.sexpr && entry.loc) {
334
+ this.recordSubMappings(entry.code, entry.sexpr, importLineOffset);
335
+ }
336
+ importLineOffset += entry.code.split('\n').length;
337
+ }
338
+ }
325
339
  if (!this._stmtEntries) return;
326
340
  let lineOffset = this._preambleLines;
327
341
  for (let entry of this._stmtEntries) {
@@ -423,6 +437,47 @@ export class CodeEmitter {
423
437
  ripSrcCache.set(genLineInStmt, v);
424
438
  return v;
425
439
  };
440
+ // Inline type annotations (emitted when `inlineTypes: true`) inject
441
+ // identifiers into function-signature lines that have no source
442
+ // counterpart — e.g. `header(name, value, opts: { append?: boolean })`.
443
+ // Their identifiers (`append`, `boolean`, etc.) would otherwise compete
444
+ // with real body identifiers in the regex matcher below, mis-mapping
445
+ // source positions onto the type literal. Detect those brace ranges
446
+ // per line and skip matches inside them.
447
+ const inlineTypeRangesCache = new Map();
448
+ const getInlineTypeRanges = (genLineInStmt) => {
449
+ if (inlineTypeRangesCache.has(genLineInStmt)) return inlineTypeRangesCache.get(genLineInStmt);
450
+ const lt = codeLines[genLineInStmt];
451
+ const ranges = [];
452
+ // Only look at function-signature shaped lines: contain `(...) {` or `(...) =>`.
453
+ if (lt && /\)\s*(\{|=>)\s*$/.test(lt)) {
454
+ // Find `: {` after an identifier (with optional `?` and whitespace).
455
+ const annotRe = /\b[a-zA-Z_$][\w$]*\??\s*:\s*\{/g;
456
+ let am;
457
+ while ((am = annotRe.exec(lt)) !== null) {
458
+ const braceStart = am.index + am[0].length - 1; // position of `{`
459
+ // Skip inside strings (defensive — unlikely here)
460
+ if (CodeEmitter._isColInsideString(lt, braceStart)) continue;
461
+ // Walk to matching `}` honoring brace depth and strings
462
+ let depth = 1, j = braceStart + 1, inStr = false, quote = '';
463
+ while (j < lt.length && depth > 0) {
464
+ const ch = lt[j];
465
+ if (inStr) {
466
+ if (ch === '\\') { j += 2; continue; }
467
+ if (ch === quote) inStr = false;
468
+ } else if (ch === '"' || ch === "'" || ch === '`') {
469
+ inStr = true; quote = ch;
470
+ } else if (ch === '{') depth++;
471
+ else if (ch === '}') depth--;
472
+ j++;
473
+ }
474
+ ranges.push([braceStart, j]); // exclude positions [braceStart, j)
475
+ annotRe.lastIndex = j;
476
+ }
477
+ }
478
+ inlineTypeRangesCache.set(genLineInStmt, ranges);
479
+ return ranges;
480
+ };
426
481
  for (let { name, origLine, origCol } of subs) {
427
482
  let escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
428
483
  let re = new RegExp('\\b' + escaped + '\\b', 'g');
@@ -442,6 +497,11 @@ export class CodeEmitter {
442
497
  const annotSrc = getRipSrcAnnot(genLineInStmt);
443
498
  const annotMatches = annotSrc != null && annotSrc === origLine;
444
499
  if (lineText && CodeEmitter._isColInsideString(lineText, genCol) && !annotMatches) continue;
500
+ // Skip matches inside inline type annotation brace ranges (e.g.
501
+ // `opts: { append?: boolean }`) — those identifiers are emitted
502
+ // for TS type-checking only and have no real source counterpart.
503
+ const itRanges = getInlineTypeRanges(genLineInStmt);
504
+ if (itRanges.length && itRanges.some(([s, e]) => genCol >= s && genCol < e)) continue;
445
505
  let genLine = lineOffset + genLineInStmt;
446
506
  // Annotation-matched lines are the authoritative gen position for
447
507
  // their source line — score them as a perfect line match so they
@@ -501,14 +561,37 @@ export class CodeEmitter {
501
561
  }
502
562
  }
503
563
  // Operators/keywords: anchor is the subject at index 1
504
- else if (typeof head === 'string' && /^[=+\-*/%<>!&|?~^]|^\.\.?$|^def$|^class$|^state$|^computed$|^readonly$|^for-/.test(head)) {
505
- if (typeof node[1] === 'string' && /^[a-zA-Z_$]/.test(node[1])) ident = node[1];
564
+ else if (typeof head === 'string' && /^[=+\-*/%<>!&|?~^]|^\.{1,3}$|^def$|^class$|^state$|^computed$|^readonly$|^for-/.test(head)) {
565
+ if (typeof node[1] === 'string' && /^[a-zA-Z_$]/.test(node[1])) {
566
+ ident = node[1];
567
+ // Spread `...x`: node.loc.c marks the `...` start; shift past the
568
+ // operator so the anchor lands on the operand identifier.
569
+ if (head === '...') identCol = node.loc.c + 3;
570
+ }
506
571
  }
507
572
  // Function call (head is identifier)
508
573
  else if (typeof head === 'string' && /^[a-zA-Z_$]/.test(head)) {
509
574
  ident = head;
510
575
  }
511
576
  if (ident) result.push({ name: ident, origLine: node.loc.r, origCol: identCol });
577
+
578
+ // Arrow body bare-identifier anchor: a single-expression arrow body
579
+ // like `-> products` parses as `['->', [], ['block', 'products']]`.
580
+ // The body atom has no .loc (parser only attaches loc to arrays), and
581
+ // the `block` wrapper has bogus `loc=0:0`, so the identifier reference
582
+ // is invisible to the heuristic mapping. Synthesize an anchor by
583
+ // scanning source forward from the arrow's location.
584
+ if ((head === '->' || head === '=>') && Array.isArray(node[2]) && str(node[2][0]) === 'block') {
585
+ const body = node[2];
586
+ for (let i = 1; i < body.length; i++) {
587
+ const leaf = body[i];
588
+ const leafStr = typeof leaf === 'string' || leaf instanceof String ? str(leaf) : null;
589
+ if (leafStr && /^[a-zA-Z_$][\w$]*$/.test(leafStr) && !leaf.loc) {
590
+ const anchor = this._scanForIdentAfter(leafStr, node.loc);
591
+ if (anchor) result.push(anchor);
592
+ }
593
+ }
594
+ }
512
595
  }
513
596
  // Side-channel anchors attached by walkRender for bare-identifier
514
597
  // children of template-tag nodes (e.g. `error` in `p.error error`).
@@ -528,6 +611,30 @@ export class CodeEmitter {
528
611
  }
529
612
  }
530
613
 
614
+ // Scan original source for the first occurrence of `ident` after the
615
+ // given start location (typically an arrow's `->` loc). Skips matches
616
+ // inside string/comment regions. Returns a sub-expression anchor or null.
617
+ _scanForIdentAfter(ident, startLoc) {
618
+ const source = this.options && this.options.source;
619
+ if (!source || !startLoc) return null;
620
+ const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
621
+ const re = new RegExp('\\b' + ident.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
622
+ const startRow = startLoc.r;
623
+ const startCol = startLoc.c;
624
+ // Search current line from startCol forward, then up to 20 subsequent lines.
625
+ for (let r = startRow; r < Math.min(lines.length, startRow + 20); r++) {
626
+ const line = lines[r];
627
+ if (!line) continue;
628
+ re.lastIndex = r === startRow ? startCol : 0;
629
+ let m;
630
+ while ((m = re.exec(line)) !== null) {
631
+ if (CodeEmitter._isColInsideString(line, m.index)) continue;
632
+ return { name: ident, origLine: r, origCol: m.index };
633
+ }
634
+ }
635
+ return null;
636
+ }
637
+
531
638
  // ---------------------------------------------------------------------------
532
639
  // Variable collection
533
640
  // ---------------------------------------------------------------------------
@@ -577,7 +684,12 @@ export class CodeEmitter {
577
684
  }
578
685
 
579
686
  if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
580
- this.collectVarsFromLoopHead(rest[0], this.programVars);
687
+ // Don't hoist loop vars: emitForIn/emitForOf/emitForAs already
688
+ // emit `for (let x of ...)`, which is block-scoped AND gives JS's
689
+ // per-iteration binding semantics (critical for closures captured
690
+ // inside the loop). Hoisting `let x` to the surrounding scope
691
+ // would shadow that as dead code (TS6133 "declared but never
692
+ // read") without changing runtime behavior.
581
693
  rest.slice(1).forEach(item => this.collectProgramVariables(item));
582
694
  return;
583
695
  }
@@ -640,7 +752,10 @@ export class CodeEmitter {
640
752
  return;
641
753
  }
642
754
  if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
643
- this.collectVarsFromLoopHead(rest[0], vars);
755
+ // See collectProgramVariables for-loop branch: loop vars are
756
+ // block-scoped via the for-header's `let`; hoisting them here
757
+ // would only produce a redundant outer `let x` flagged as
758
+ // unused.
644
759
  rest.slice(1).forEach(collect);
645
760
  return;
646
761
  }
@@ -668,20 +783,55 @@ export class CodeEmitter {
668
783
  return vars;
669
784
  }
670
785
 
786
+ // Walk a function body and collect typed local assignments. Returns a
787
+ // Map<name, typeString> for every `name:: T = value` whose target is a
788
+ // String-wrapped identifier carrying `data.type` (attached by types.js).
789
+ //
790
+ // Used by `emitBodyWithReturns` in `inlineTypes` mode to annotate the
791
+ // function-top hoist (`let a, y: boolean, b;`) so shadow-TS sees the
792
+ // intended type instead of inferring a literal from the first RHS. Stops
793
+ // at nested function boundaries — each function owns its own typed locals.
794
+ //
795
+ // Conflict policy: first annotation wins. Same-name re-annotations are
796
+ // silently ignored; mixing different types on the same local is an
797
+ // unusual pattern best surfaced by TS itself once the hoist carries the
798
+ // first annotation.
799
+ collectTypedLocals(body) {
800
+ let typed = new Map();
801
+ let walk = (sexpr) => {
802
+ if (!Array.isArray(sexpr)) return;
803
+ let [head, ...rest] = sexpr;
804
+ head = str(head);
805
+ if (Array.isArray(head)) { sexpr.forEach(walk); return; }
806
+ if (CodeEmitter.ASSIGNMENT_OPS.has(head)) {
807
+ let [target, value] = rest;
808
+ if (target instanceof String && target.type && !typed.has(str(target))) {
809
+ typed.set(str(target), target.type);
810
+ }
811
+ walk(value);
812
+ return;
813
+ }
814
+ if (head === 'def' || head === '->' || head === '=>' || head === 'effect') return;
815
+ rest.forEach(walk);
816
+ };
817
+ walk(body);
818
+ return typed;
819
+ }
820
+
671
821
  // ---------------------------------------------------------------------------
672
822
  // Main dispatch
673
823
  // ---------------------------------------------------------------------------
674
824
 
675
825
  emit(sexpr, context = 'statement') {
676
- // String object with metadata (quote, await, predicate, heregex, etc.)
826
+ // String object with metadata (quote, bang, optional, heregex, etc.)
677
827
  if (sexpr instanceof String) {
678
828
  // Dammit operator (!)
679
- if (meta(sexpr, 'await') === true) {
829
+ if (meta(sexpr, 'bang') === true) {
680
830
  return `await ${str(sexpr)}()`;
681
831
  }
682
832
 
683
833
  // Existence check (?)
684
- if (meta(sexpr, 'predicate')) {
834
+ if (meta(sexpr, 'optional')) {
685
835
  return `(${str(sexpr)} != null)`;
686
836
  }
687
837
 
@@ -737,8 +887,8 @@ export class CodeEmitter {
737
887
 
738
888
  let [head, ...rest] = sexpr;
739
889
 
740
- // Preserve await metadata before converting head to primitive
741
- let headAwaitMeta = meta(head, 'await');
890
+ // Preserve bang metadata before converting head to primitive
891
+ let headBangMeta = meta(head, 'bang');
742
892
  head = str(head);
743
893
 
744
894
  // Dispatch table
@@ -758,7 +908,7 @@ export class CodeEmitter {
758
908
  let postfix = this._tryPostfixCall(head, rest, context);
759
909
  if (postfix) return postfix;
760
910
 
761
- let needsAwait = headAwaitMeta === true;
911
+ let needsAwait = headBangMeta === true;
762
912
  let callStr = `${this.emit(head, 'value')}(${this._emitArgs(rest)})`;
763
913
  return needsAwait ? `await ${callStr}` : callStr;
764
914
  }
@@ -784,15 +934,16 @@ export class CodeEmitter {
784
934
  let postfix = this._tryPostfixCall(head, rest, context);
785
935
  if (postfix) return postfix;
786
936
 
787
- // Property access with await sigil on property
937
+ // Property access with bang sigil on property
788
938
  let needsAwait = false;
789
939
  let calleeCode;
790
- if (head[0] === '.' && meta(head[2], 'await') === true) {
940
+ if (head[0] === '.' && meta(head[2], 'bang') === true) {
791
941
  needsAwait = true;
792
942
  let [obj, prop] = head.slice(1);
793
943
  let objCode = this.emit(obj, 'value');
794
944
  let needsParens = CodeEmitter.NUMBER_LITERAL_RE.test(objCode) ||
795
- ((this.is(obj, 'object') || this.is(obj, 'await') || this.is(obj, 'yield')));
945
+ ((this.is(obj, 'object') || this.is(obj, 'await') || this.is(obj, 'yield'))) ||
946
+ /^(await|yield)\s/.test(objCode);
796
947
  let base = needsParens ? `(${objCode})` : objCode;
797
948
  calleeCode = `${base}.${str(prop)}`;
798
949
  } else {
@@ -850,7 +1001,12 @@ export class CodeEmitter {
850
1001
  let needsBlank = false;
851
1002
 
852
1003
  if (imports.length > 0) {
853
- code += imports.map(s => this.addSemicolon(s, this.emit(s, 'statement'))).join('\n');
1004
+ let importEntries = imports.map(s => {
1005
+ let generated = this.addSemicolon(s, this.emit(s, 'statement'));
1006
+ return { code: generated, loc: Array.isArray(s) ? s.loc : null, sexpr: Array.isArray(s) ? s : null };
1007
+ });
1008
+ this._importEntries = importEntries;
1009
+ code += importEntries.map(e => e.code).join('\n');
854
1010
  needsBlank = true;
855
1011
  }
856
1012
 
@@ -1042,15 +1198,7 @@ export class CodeEmitter {
1042
1198
  }
1043
1199
 
1044
1200
  // Validate: no sigils in assignment targets (except void function syntax)
1045
- let isFnValue = (this.is(value, '->') || this.is(value, '=>') || this.is(value, 'def'));
1046
- if (target instanceof String && meta(target, 'await') !== undefined && !isFnValue) {
1047
- let sigil = meta(target, 'await') === true ? '!' : '&';
1048
- this.error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'`, sexpr);
1049
- }
1050
-
1051
- if (target instanceof String && meta(target, 'await') === true && isFnValue) {
1052
- this.nextFunctionIsVoid = true;
1053
- }
1201
+ this.applyVoidMarker(target, value, sexpr);
1054
1202
 
1055
1203
  // Empty destructuring — just evaluate RHS
1056
1204
  let isEmptyArr = this.is(target, 'array', 0);
@@ -1139,7 +1287,7 @@ export class CodeEmitter {
1139
1287
 
1140
1288
  // Generate target (handle reactive, sigils)
1141
1289
  let targetCode;
1142
- if (target instanceof String && meta(target, 'await') !== undefined) {
1290
+ if (target instanceof String && meta(target, 'bang') !== undefined) {
1143
1291
  targetCode = str(target);
1144
1292
  } else if (typeof target === 'string' && this.reactiveVars?.has(target)) {
1145
1293
  targetCode = `${target}.value`;
@@ -1186,8 +1334,8 @@ export class CodeEmitter {
1186
1334
  objCode.startsWith('await ') ||
1187
1335
  ((this.is(obj, 'object') || this.is(obj, 'yield')));
1188
1336
  let base = needsParens ? `(${objCode})` : objCode;
1189
- if (meta(prop, 'await') === true) return `await ${base}.${str(prop)}()`;
1190
- if (meta(prop, 'predicate')) return `(${base}.${str(prop)} != null)`;
1337
+ if (meta(prop, 'bang') === true) return `await ${base}.${str(prop)}()`;
1338
+ if (meta(prop, 'optional')) return `(${base}.${str(prop)} != null)`;
1191
1339
  return `${base}.${str(prop)}`;
1192
1340
  }
1193
1341
 
@@ -1353,7 +1501,7 @@ export class CodeEmitter {
1353
1501
 
1354
1502
  emitDef(head, rest, context, sexpr) {
1355
1503
  let [name, params, body] = rest;
1356
- let sideEffectOnly = meta(name, 'await') === true;
1504
+ let sideEffectOnly = meta(name, 'bang') === true;
1357
1505
  let cleanName = str(name);
1358
1506
  let paramList = this.emitParamList(params);
1359
1507
  let bodyCode = this.emitFunctionBody(body, params, sideEffectOnly);
@@ -1365,8 +1513,7 @@ export class CodeEmitter {
1365
1513
  emitThinArrow(head, rest, context, sexpr) {
1366
1514
  let [params, body] = rest;
1367
1515
  if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(body)) params = ['it'];
1368
- let sideEffectOnly = this.nextFunctionIsVoid || false;
1369
- this.nextFunctionIsVoid = false;
1516
+ let sideEffectOnly = sexpr.isVoid || false;
1370
1517
  let paramList = this.emitParamList(params);
1371
1518
  let bodyCode = this.emitFunctionBody(body, params, sideEffectOnly);
1372
1519
  let isAsync = this.containsAwait(body);
@@ -1378,8 +1525,7 @@ export class CodeEmitter {
1378
1525
  emitFatArrow(head, rest, context, sexpr) {
1379
1526
  let [params, body] = rest;
1380
1527
  if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(body)) params = ['it'];
1381
- let sideEffectOnly = this.nextFunctionIsVoid || false;
1382
- this.nextFunctionIsVoid = false;
1528
+ let sideEffectOnly = sexpr.isVoid || false;
1383
1529
  let paramList = this.emitParamList(params);
1384
1530
  let isSingle = params.length === 1 && typeof params[0] === 'string' &&
1385
1531
  !paramList.includes('=') && !paramList.includes('...') &&
@@ -1896,7 +2042,20 @@ export class CodeEmitter {
1896
2042
  // Symbol literals
1897
2043
  // ---------------------------------------------------------------------------
1898
2044
 
1899
- emitSymbol(head, rest) { return `Symbol.for(${JSON.stringify(rest[0])})`; }
2045
+ emitSymbol(head, rest, context, sexpr) {
2046
+ // Anchor the symbol name's source position to the generated `Symbol`
2047
+ // identifier so hover on `:foo` shows `SymbolConstructor` (the
2048
+ // generated `"foo"` is a string literal that TS has no hover for).
2049
+ // sexpr.loc points at the `:`; the name starts at col + 1.
2050
+ if (sexpr && sexpr.loc && typeof rest[0] === 'string') {
2051
+ sexpr._anchors = (sexpr._anchors || []).concat([{
2052
+ name: 'Symbol',
2053
+ origLine: sexpr.loc.r,
2054
+ origCol: sexpr.loc.c + 1,
2055
+ }]);
2056
+ }
2057
+ return `Symbol.for(${JSON.stringify(rest[0])})`;
2058
+ }
1900
2059
 
1901
2060
  // ---------------------------------------------------------------------------
1902
2061
  // Data structures
@@ -1996,8 +2155,7 @@ export class CodeEmitter {
1996
2155
  if (operator === ':' && isSimpleKey && this.is(value, '->')) {
1997
2156
  let [, mParams, mBody] = value;
1998
2157
  if ((!mParams || (Array.isArray(mParams) && mParams.length === 0)) && this.containsIt(mBody)) mParams = ['it'];
1999
- let mSideEffect = this.nextFunctionIsVoid || false;
2000
- this.nextFunctionIsVoid = false;
2158
+ let mSideEffect = value.isVoid || false;
2001
2159
  let mParamList = this.emitParamList(mParams);
2002
2160
  let mBodyCode = this.emitFunctionBody(mBody, mParams, mSideEffect);
2003
2161
  let mIsAsync = this.containsAwait(mBody);
@@ -2578,7 +2736,7 @@ export class CodeEmitter {
2578
2736
  emitImport(head, rest, context, sexpr) {
2579
2737
  if (rest.length === 1) {
2580
2738
  let importExpr = `import(${this.emit(rest[0], 'value')})`;
2581
- if (meta(sexpr[0], 'await') === true) return `(await ${importExpr})`;
2739
+ if (meta(sexpr[0], 'bang') === true) return `(await ${importExpr})`;
2582
2740
  return importExpr;
2583
2741
  }
2584
2742
  if (this.options.skipImports) return '';
@@ -2587,6 +2745,7 @@ export class CodeEmitter {
2587
2745
  let fixedSource = this.addJsExtensionAndAssertions(source);
2588
2746
  if (named[0] === '*' && named.length === 2) return `import ${def}, * as ${named[1]} from ${fixedSource}`;
2589
2747
  let names = named.map(i => Array.isArray(i) && i.length === 2 ? `${i[0]} as ${i[1]}` : i).join(', ');
2748
+ this._attachImportSpecifierAnchors(sexpr, [def, ...named.flatMap(i => Array.isArray(i) ? i : [i])]);
2590
2749
  return `import ${def}, { ${names} } from ${fixedSource}`;
2591
2750
  }
2592
2751
  let [specifier, source] = rest;
@@ -2595,11 +2754,70 @@ export class CodeEmitter {
2595
2754
  if (Array.isArray(specifier)) {
2596
2755
  if (specifier[0] === '*' && specifier.length === 2) return `import * as ${specifier[1]} from ${fixedSource}`;
2597
2756
  let names = specifier.map(i => Array.isArray(i) && i.length === 2 ? `${i[0]} as ${i[1]}` : i).join(', ');
2757
+ this._attachImportSpecifierAnchors(sexpr, specifier.flatMap(i => Array.isArray(i) ? i : [i]));
2598
2758
  return `import { ${names} } from ${fixedSource}`;
2599
2759
  }
2600
2760
  return `import ${this.emit(specifier, 'value')} from ${fixedSource}`;
2601
2761
  }
2602
2762
 
2763
+ // Attach source-map anchors for each named import specifier so hover /
2764
+ // go-to-def works on individual names in `import { foo, bar } from '...'`.
2765
+ // The parser drops .loc from specifier strings, so we recover positions
2766
+ // by scanning the source forward from the `import` keyword.
2767
+ _attachImportSpecifierAnchors(sexpr, names) {
2768
+ if (!sexpr || !sexpr.loc) return;
2769
+ const source = this.options && this.options.source;
2770
+ if (!source || !names || !names.length) return;
2771
+ const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
2772
+ let row = sexpr.loc.r;
2773
+ let col = sexpr.loc.c;
2774
+ const anchors = [];
2775
+ for (const name of names) {
2776
+ if (typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name)) continue;
2777
+ const re = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
2778
+ let found = false;
2779
+ while (row < lines.length) {
2780
+ const line = lines[row] || '';
2781
+ // Strip trailing line comment so '# foo' doesn't match
2782
+ const codePart = line.replace(/#.*$/, '');
2783
+ re.lastIndex = 0;
2784
+ const slice = codePart.slice(col);
2785
+ const m = re.exec(slice);
2786
+ if (m) {
2787
+ const c = col + m.index;
2788
+ anchors.push({ name, origLine: row, origCol: c });
2789
+ col = c + name.length;
2790
+ found = true;
2791
+ break;
2792
+ }
2793
+ row++;
2794
+ col = 0;
2795
+ }
2796
+ if (!found) break;
2797
+ }
2798
+ if (anchors.length) sexpr._anchors = (sexpr._anchors || []).concat(anchors);
2799
+ }
2800
+
2801
+ // Propagate the void marker from a `name! = fn` LHS onto the function node.
2802
+ // The `!` suffix is recorded as `.bang === true` metadata on the target
2803
+ // identifier; when the value is a function (`->`/`=>`/`def`) the bang means
2804
+ // the function is void (no implicit return). We stamp `isVoid` directly on
2805
+ // the function node so the arrow emitters read it locally — the same way
2806
+ // `emitDef` reads `meta(name, 'bang')` off its own node. Used by assignment
2807
+ // and export declaration paths so `export name! = ->` matches the bare
2808
+ // `name! = ->` semantics. Rejects `!`/`&` sigils on non-function values,
2809
+ // exactly like a plain assignment.
2810
+ applyVoidMarker(target, value, sexpr) {
2811
+ let isFnValue = (this.is(value, '->') || this.is(value, '=>') || this.is(value, 'def'));
2812
+ if (target instanceof String && meta(target, 'bang') !== undefined && !isFnValue) {
2813
+ let sigil = meta(target, 'bang') === true ? '!' : '&';
2814
+ this.error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'`, sexpr);
2815
+ }
2816
+ if (target instanceof String && meta(target, 'bang') === true && isFnValue) {
2817
+ value.isVoid = true;
2818
+ }
2819
+ }
2820
+
2603
2821
  emitExport(head, rest) {
2604
2822
  let [decl] = rest;
2605
2823
  if (this.options.skipExports) {
@@ -2612,6 +2830,7 @@ export class CodeEmitter {
2612
2830
  this._componentTypeParams = decl[1]?.typeParams || '';
2613
2831
  }
2614
2832
  if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
2833
+ this.applyVoidMarker(decl[1], decl[2], decl);
2615
2834
  const result = `const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
2616
2835
  this._componentName = prev;
2617
2836
  this._componentTypeParams = prevTP;
@@ -2630,6 +2849,7 @@ export class CodeEmitter {
2630
2849
  this._componentTypeParams = decl[1]?.typeParams || '';
2631
2850
  }
2632
2851
  if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
2852
+ this.applyVoidMarker(decl[1], decl[2], decl);
2633
2853
  const result = `export const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
2634
2854
  this._componentName = prev;
2635
2855
  this._componentTypeParams = prevTP;
@@ -2643,10 +2863,14 @@ export class CodeEmitter {
2643
2863
  emitExportDefault(head, rest) {
2644
2864
  let [expr] = rest;
2645
2865
  if (this.options.skipExports) {
2646
- if (this.is(expr, '=')) return `const ${expr[1]} = ${this.emit(expr[2], 'value')}`;
2866
+ if (this.is(expr, '=')) {
2867
+ this.applyVoidMarker(expr[1], expr[2], expr);
2868
+ return `const ${expr[1]} = ${this.emit(expr[2], 'value')}`;
2869
+ }
2647
2870
  return this.emit(expr, 'statement');
2648
2871
  }
2649
2872
  if (this.is(expr, '=')) {
2873
+ this.applyVoidMarker(expr[1], expr[2], expr);
2650
2874
  return `const ${expr[1]} = ${this.emit(expr[2], 'value')};\nexport default ${expr[1]}`;
2651
2875
  }
2652
2876
  return `export default ${this.emit(expr, 'statement')}`;
@@ -2776,14 +3000,36 @@ export class CodeEmitter {
2776
3000
 
2777
3001
  formatParam(param) {
2778
3002
  if (typeof param === 'string') return param;
2779
- if (param instanceof String) return param.valueOf();
2780
- if (this.is(param, 'rest')) return `...${param[1]}`;
3003
+ if (param instanceof String) {
3004
+ // In `inlineTypes` mode (set by typecheck.compileForCheck), emit the
3005
+ // type annotation inline so shadow TS sees `name: T` for typed params
3006
+ // in every function-like position — top-level arrows, class methods,
3007
+ // object-literal method shorthand, nested functions, etc. The user-
3008
+ // facing `-c` output stays untouched because this flag is off by
3009
+ // default. `.type` carries the raw Rip type string (with `::`),
3010
+ // which converts to TS form by swapping `::` → `:`.
3011
+ if (this.options.inlineTypes && param.type) {
3012
+ return `${param.valueOf()}: ${param.type.replace(/::/g, ':')}`;
3013
+ }
3014
+ return param.valueOf();
3015
+ }
3016
+ if (this.is(param, 'rest')) {
3017
+ // Rest param: `...name`. When the name is a String wrapper carrying
3018
+ // a type, emit `...name: T` so shadow TS sees the rest tuple/array.
3019
+ let restName = param[1];
3020
+ if (this.options.inlineTypes && restName instanceof String && restName.type) {
3021
+ return `...${restName.valueOf()}: ${restName.type.replace(/::/g, ':')}`;
3022
+ }
3023
+ return `...${restName}`;
3024
+ }
2781
3025
  if (this.is(param, 'default')) {
2782
3026
  // `param[1]` is either a plain identifier string (e.g. `x = 5`) or a
2783
3027
  // destructuring pattern AST node (e.g. `{a, b} = {}`). Recurse via
2784
3028
  // `formatParam` so patterns emit as `{a, b}` / `[x, y]` instead of
2785
3029
  // being coerced to a string via `Array.prototype.toString`, which
2786
3030
  // produced the famous `(object,,a,a,,b,b = {})` mis-rendering.
3031
+ // The recursion also picks up any inline type annotation on the
3032
+ // name in `inlineTypes` mode, yielding `name: T = default`.
2787
3033
  return `${this.formatParam(param[1])} = ${this.emit(param[2], 'value')}`;
2788
3034
  }
2789
3035
  if (this.is(param, '.') && param[1] === 'this') return param[2];
@@ -2823,10 +3069,14 @@ export class CodeEmitter {
2823
3069
 
2824
3070
  let paramNames = new Set();
2825
3071
  let extractPN = (p) => {
2826
- if (typeof p === 'string') paramNames.add(p);
3072
+ // Unwrap String wrappers — typed params arrive as `new String('name')`
3073
+ // with `.data.type` metadata. Without unwrapping, `paramNames.has('name')`
3074
+ // misses (Set compares wrappers by identity), causing the param name to
3075
+ // be re-hoisted as a local `let`, producing duplicate-declaration errors.
3076
+ if (typeof p === 'string' || p instanceof String) paramNames.add(str(p));
2827
3077
  else if (Array.isArray(p)) {
2828
- if (p[0] === 'rest' || p[0] === '...') { if (typeof p[1] === 'string') paramNames.add(p[1]); }
2829
- else if (p[0] === 'default') { if (typeof p[1] === 'string') paramNames.add(p[1]); }
3078
+ if (p[0] === 'rest' || p[0] === '...') { if (typeof p[1] === 'string' || p[1] instanceof String) paramNames.add(str(p[1])); }
3079
+ else if (p[0] === 'default') { if (typeof p[1] === 'string' || p[1] instanceof String) paramNames.add(str(p[1])); }
2830
3080
  else if (p[0] === 'array' || p[0] === 'object') this.collectVarsFromArray(p, paramNames);
2831
3081
  }
2832
3082
  };
@@ -2870,7 +3120,20 @@ export class CodeEmitter {
2870
3120
 
2871
3121
  this.indentLevel++;
2872
3122
  let code = '{\n';
2873
- if (newVars.size > 0) code += this.indent() + `let ${Array.from(newVars).sort().join(', ')};\n`;
3123
+ if (newVars.size > 0) {
3124
+ // In `inlineTypes` mode, propagate `name:: T = value` annotations from
3125
+ // body-level typed assignments onto the hoisted `let`. Without this,
3126
+ // the hoist emits `let y;` (no type), shadow-TS infers from the first
3127
+ // RHS literal (`y = true` → `y: true`), and any later `y = false`
3128
+ // fails TS2322. With this, the hoist emits `let y: boolean;` and the
3129
+ // body's `y = true` / `y = false` both check cleanly.
3130
+ let typedLocals = this.options.inlineTypes ? this.collectTypedLocals(body) : null;
3131
+ let names = Array.from(newVars).sort().map(n => {
3132
+ let t = typedLocals?.get(n);
3133
+ return t ? `${n}: ${t}` : n;
3134
+ });
3135
+ code += this.indent() + `let ${names.join(', ')};\n`;
3136
+ }
2874
3137
 
2875
3138
  let firstIsSuper = autoAssignments.length > 0 && statements.length > 0 &&
2876
3139
  Array.isArray(statements[0]) && statements[0][0] === 'super';
@@ -3707,7 +3970,7 @@ export class CodeEmitter {
3707
3970
 
3708
3971
  containsAwait(sexpr) {
3709
3972
  if (!sexpr) return false;
3710
- if (sexpr instanceof String && meta(sexpr, 'await') === true) return true;
3973
+ if (sexpr instanceof String && meta(sexpr, 'bang') === true) return true;
3711
3974
  if (typeof sexpr !== 'object') return false;
3712
3975
  if (this.is(sexpr, 'await')) return true;
3713
3976
  if (this.is(sexpr, 'for-as') && sexpr[3] === true) return true;
@@ -4166,7 +4429,13 @@ export class Compiler {
4166
4429
  // in this file at all — the exporting module strips its `export type` to
4167
4430
  // nothing, so leaving the specifier in place would cause a runtime
4168
4431
  // "module does not provide an export named X" error.
4169
- if (lexer.typeRefNames?.size > 0) {
4432
+ //
4433
+ // Skip elision in `inlineTypes` mode (set by typecheck.compileForCheck).
4434
+ // The shadow-TS output is fed to the TypeScript language service, not
4435
+ // executed — so keeping the specifiers preserves go-to-definition and
4436
+ // hover for type-only imports like `RetryConfig` that resolve to
4437
+ // `export type` declarations in the target module.
4438
+ if (lexer.typeRefNames?.size > 0 && !this.options.inlineTypes) {
4170
4439
  let usedNames = new Set();
4171
4440
  let inImport = false;
4172
4441
  for (let t of tokens) {
@@ -4377,6 +4646,11 @@ export class Compiler {
4377
4646
  skipDataPart: this.options.skipDataPart,
4378
4647
  stubComponents: this.options.stubComponents,
4379
4648
  reactiveVars: this.options.reactiveVars,
4649
+ // Emit `name: T` inline on typed params so shadow TS in compileForCheck
4650
+ // sees annotations on every function-like position (top-level arrows,
4651
+ // class methods, object-literal method shorthand, nested functions).
4652
+ // Off by default — only set when producing input for the shadow TS pass.
4653
+ inlineTypes: this.options.inlineTypes,
4380
4654
  // Schema runtime mode: 'browser' / 'validate' / 'server' / 'migration'.
4381
4655
  // Default 'migration' covers the common case (CLI, server, tests) where
4382
4656
  // the user might call any schema feature including .toSQL(). The browser