rip-lang 3.16.0 → 3.16.2

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.
Files changed (57) hide show
  1. package/README.md +3 -4
  2. package/bin/rip +201 -18
  3. package/bin/rip-schema +175 -0
  4. package/docs/AGENTS.md +1 -1
  5. package/docs/RIP-APP.md +200 -19
  6. package/docs/RIP-DUCKDB.md +64 -1
  7. package/docs/RIP-INTRO.md +4 -4
  8. package/docs/RIP-LANG.md +36 -38
  9. package/docs/RIP-SCHEMA.md +1204 -364
  10. package/docs/RIP-TYPES.md +74 -103
  11. package/docs/demo/README.md +4 -3
  12. package/docs/dist/rip.js +4195 -966
  13. package/docs/dist/rip.min.js +1161 -284
  14. package/docs/dist/rip.min.js.br +0 -0
  15. package/docs/example/index.json +7 -7
  16. package/docs/example/index.json.br +0 -0
  17. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  18. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  19. package/docs/extensions/vscode/rip/index.html +2 -1
  20. package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
  21. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  22. package/docs/extensions/vscode/rip/vscode-rip-0.6.0.vsix +0 -0
  23. package/docs/index.html +1 -1
  24. package/docs/ui/bundle.json +55 -55
  25. package/docs/ui/bundle.json.br +0 -0
  26. package/docs/ui/hljs-rip.js +1 -1
  27. package/docs/ui/index.html +1 -1
  28. package/package.json +15 -7
  29. package/rip-loader.js +59 -2
  30. package/src/AGENTS.md +43 -12
  31. package/src/browser.js +52 -11
  32. package/src/compiler.js +538 -80
  33. package/src/components.js +488 -48
  34. package/src/dts.js +80 -50
  35. package/src/grammar/README.md +29 -170
  36. package/src/grammar/grammar.rip +17 -12
  37. package/src/grammar/solar.rip +4 -17
  38. package/src/lexer.js +82 -32
  39. package/src/parser.js +229 -229
  40. package/src/schema/dts.js +328 -54
  41. package/src/schema/loader-server.js +2 -1
  42. package/src/schema/runtime-browser-stubs.js +20 -9
  43. package/src/schema/runtime-ddl.js +161 -44
  44. package/src/schema/runtime-migrate.js +681 -0
  45. package/src/schema/runtime-orm.js +698 -54
  46. package/src/schema/runtime-validate.js +808 -24
  47. package/src/schema/runtime.generated.js +2395 -135
  48. package/src/schema/schema.js +1054 -94
  49. package/src/typecheck.js +1610 -127
  50. package/src/types.js +90 -6
  51. package/src/grammar/lunar.rip +0 -2412
  52. /package/docs/demo/{components → routes}/_layout.rip +0 -0
  53. /package/docs/demo/{components → routes}/about.rip +0 -0
  54. /package/docs/demo/{components → routes}/card.rip +0 -0
  55. /package/docs/demo/{components → routes}/counter.rip +0 -0
  56. /package/docs/demo/{components → routes}/index.rip +0 -0
  57. /package/docs/demo/{components → routes}/todos.rip +0 -0
package/src/compiler.js CHANGED
@@ -16,7 +16,7 @@ import { installComponentSupport } from './components.js';
16
16
  // so _typesEmitter stays null and .d.ts output is silently skipped.
17
17
  let _typesEmitter = null;
18
18
  export function setTypesEmitter(fn) { _typesEmitter = fn; }
19
- import { installSchemaSupport } from './schema/schema.js';
19
+ import { installSchemaSupport, foldDerivedSchemas } from './schema/schema.js';
20
20
  import { SourceMapGenerator } from './sourcemaps.js';
21
21
  import { stringify, getStdlibCode } from './stdlib.js';
22
22
  import { RipError, toRipError } from './error.js';
@@ -220,6 +220,7 @@ export class CodeEmitter {
220
220
  'computed': 'emitComputed',
221
221
  'readonly': 'emitReadonly',
222
222
  'effect': 'emitEffect',
223
+ 'gate': 'emitGate',
223
224
 
224
225
  // Control flow — simple
225
226
  'break': 'emitBreak',
@@ -227,7 +228,6 @@ export class CodeEmitter {
227
228
  '?': 'emitExistential',
228
229
  'presence': 'emitPresence',
229
230
  '?:': 'emitTernary',
230
- '|>': 'emitPipe',
231
231
  'loop': 'emitLoop',
232
232
  'loop-n': 'emitLoopN',
233
233
  'await': 'emitAwait',
@@ -309,6 +309,11 @@ export class CodeEmitter {
309
309
  this.functionVars = new Map();
310
310
  this.helpers = new Set();
311
311
  this.scopeStack = []; // Track enclosing function scopes for proper variable hoisting
312
+ // Opt-in projection folding: rewrite foldable `X = Base.pick(...)` derived
313
+ // schemas into self-contained schema literals before emit (and before DTS,
314
+ // which reads the same s-expr). Off by default — the browser-bundle
315
+ // extractor enables it so projections can cross the client boundary.
316
+ if (this.options.foldProjections) foldDerivedSchemas(sexpr);
312
317
  this.collectProgramVariables(sexpr);
313
318
  let code = this.emit(sexpr);
314
319
 
@@ -322,6 +327,20 @@ export class CodeEmitter {
322
327
  // Each entry pairs a statement's generated code with its source loc.
323
328
  // Output line positions are computed by exact arithmetic — no heuristics.
324
329
  buildMappings() {
330
+ // Imports are emitted at the top of the file (before the preamble),
331
+ // starting at line 0. Process them first with their own line offset.
332
+ if (this._importEntries) {
333
+ let importLineOffset = 0;
334
+ for (let entry of this._importEntries) {
335
+ if (entry.loc) {
336
+ this.sourceMap.addMapping(importLineOffset, 0, entry.loc.r, entry.loc.c);
337
+ }
338
+ if (entry.sexpr && entry.loc) {
339
+ this.recordSubMappings(entry.code, entry.sexpr, importLineOffset);
340
+ }
341
+ importLineOffset += entry.code.split('\n').length;
342
+ }
343
+ }
325
344
  if (!this._stmtEntries) return;
326
345
  let lineOffset = this._preambleLines;
327
346
  for (let entry of this._stmtEntries) {
@@ -423,6 +442,47 @@ export class CodeEmitter {
423
442
  ripSrcCache.set(genLineInStmt, v);
424
443
  return v;
425
444
  };
445
+ // Inline type annotations (emitted when `inlineTypes: true`) inject
446
+ // identifiers into function-signature lines that have no source
447
+ // counterpart — e.g. `header(name, value, opts: { append?: boolean })`.
448
+ // Their identifiers (`append`, `boolean`, etc.) would otherwise compete
449
+ // with real body identifiers in the regex matcher below, mis-mapping
450
+ // source positions onto the type literal. Detect those brace ranges
451
+ // per line and skip matches inside them.
452
+ const inlineTypeRangesCache = new Map();
453
+ const getInlineTypeRanges = (genLineInStmt) => {
454
+ if (inlineTypeRangesCache.has(genLineInStmt)) return inlineTypeRangesCache.get(genLineInStmt);
455
+ const lt = codeLines[genLineInStmt];
456
+ const ranges = [];
457
+ // Only look at function-signature shaped lines: contain `(...) {` or `(...) =>`.
458
+ if (lt && /\)\s*(\{|=>)\s*$/.test(lt)) {
459
+ // Find `: {` after an identifier (with optional `?` and whitespace).
460
+ const annotRe = /\b[a-zA-Z_$][\w$]*\??\s*:\s*\{/g;
461
+ let am;
462
+ while ((am = annotRe.exec(lt)) !== null) {
463
+ const braceStart = am.index + am[0].length - 1; // position of `{`
464
+ // Skip inside strings (defensive — unlikely here)
465
+ if (CodeEmitter._isColInsideString(lt, braceStart)) continue;
466
+ // Walk to matching `}` honoring brace depth and strings
467
+ let depth = 1, j = braceStart + 1, inStr = false, quote = '';
468
+ while (j < lt.length && depth > 0) {
469
+ const ch = lt[j];
470
+ if (inStr) {
471
+ if (ch === '\\') { j += 2; continue; }
472
+ if (ch === quote) inStr = false;
473
+ } else if (ch === '"' || ch === "'" || ch === '`') {
474
+ inStr = true; quote = ch;
475
+ } else if (ch === '{') depth++;
476
+ else if (ch === '}') depth--;
477
+ j++;
478
+ }
479
+ ranges.push([braceStart, j]); // exclude positions [braceStart, j)
480
+ annotRe.lastIndex = j;
481
+ }
482
+ }
483
+ inlineTypeRangesCache.set(genLineInStmt, ranges);
484
+ return ranges;
485
+ };
426
486
  for (let { name, origLine, origCol } of subs) {
427
487
  let escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
428
488
  let re = new RegExp('\\b' + escaped + '\\b', 'g');
@@ -442,6 +502,11 @@ export class CodeEmitter {
442
502
  const annotSrc = getRipSrcAnnot(genLineInStmt);
443
503
  const annotMatches = annotSrc != null && annotSrc === origLine;
444
504
  if (lineText && CodeEmitter._isColInsideString(lineText, genCol) && !annotMatches) continue;
505
+ // Skip matches inside inline type annotation brace ranges (e.g.
506
+ // `opts: { append?: boolean }`) — those identifiers are emitted
507
+ // for TS type-checking only and have no real source counterpart.
508
+ const itRanges = getInlineTypeRanges(genLineInStmt);
509
+ if (itRanges.length && itRanges.some(([s, e]) => genCol >= s && genCol < e)) continue;
445
510
  let genLine = lineOffset + genLineInStmt;
446
511
  // Annotation-matched lines are the authoritative gen position for
447
512
  // their source line — score them as a perfect line match so they
@@ -501,14 +566,37 @@ export class CodeEmitter {
501
566
  }
502
567
  }
503
568
  // 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];
569
+ else if (typeof head === 'string' && /^[=+\-*/%<>!&|?~^]|^\.{1,3}$|^def$|^class$|^state$|^computed$|^readonly$|^for-/.test(head)) {
570
+ if (typeof node[1] === 'string' && /^[a-zA-Z_$]/.test(node[1])) {
571
+ ident = node[1];
572
+ // Spread `...x`: node.loc.c marks the `...` start; shift past the
573
+ // operator so the anchor lands on the operand identifier.
574
+ if (head === '...') identCol = node.loc.c + 3;
575
+ }
506
576
  }
507
577
  // Function call (head is identifier)
508
578
  else if (typeof head === 'string' && /^[a-zA-Z_$]/.test(head)) {
509
579
  ident = head;
510
580
  }
511
581
  if (ident) result.push({ name: ident, origLine: node.loc.r, origCol: identCol });
582
+
583
+ // Arrow body bare-identifier anchor: a single-expression arrow body
584
+ // like `-> products` parses as `['->', [], ['block', 'products']]`.
585
+ // The body atom has no .loc (parser only attaches loc to arrays), and
586
+ // the `block` wrapper has bogus `loc=0:0`, so the identifier reference
587
+ // is invisible to the heuristic mapping. Synthesize an anchor by
588
+ // scanning source forward from the arrow's location.
589
+ if ((head === '->' || head === '=>') && Array.isArray(node[2]) && str(node[2][0]) === 'block') {
590
+ const body = node[2];
591
+ for (let i = 1; i < body.length; i++) {
592
+ const leaf = body[i];
593
+ const leafStr = typeof leaf === 'string' || leaf instanceof String ? str(leaf) : null;
594
+ if (leafStr && /^[a-zA-Z_$][\w$]*$/.test(leafStr) && !leaf.loc) {
595
+ const anchor = this._scanForIdentAfter(leafStr, node.loc);
596
+ if (anchor) result.push(anchor);
597
+ }
598
+ }
599
+ }
512
600
  }
513
601
  // Side-channel anchors attached by walkRender for bare-identifier
514
602
  // children of template-tag nodes (e.g. `error` in `p.error error`).
@@ -528,6 +616,30 @@ export class CodeEmitter {
528
616
  }
529
617
  }
530
618
 
619
+ // Scan original source for the first occurrence of `ident` after the
620
+ // given start location (typically an arrow's `->` loc). Skips matches
621
+ // inside string/comment regions. Returns a sub-expression anchor or null.
622
+ _scanForIdentAfter(ident, startLoc) {
623
+ const source = this.options && this.options.source;
624
+ if (!source || !startLoc) return null;
625
+ const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
626
+ const re = new RegExp('\\b' + ident.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
627
+ const startRow = startLoc.r;
628
+ const startCol = startLoc.c;
629
+ // Search current line from startCol forward, then up to 20 subsequent lines.
630
+ for (let r = startRow; r < Math.min(lines.length, startRow + 20); r++) {
631
+ const line = lines[r];
632
+ if (!line) continue;
633
+ re.lastIndex = r === startRow ? startCol : 0;
634
+ let m;
635
+ while ((m = re.exec(line)) !== null) {
636
+ if (CodeEmitter._isColInsideString(line, m.index)) continue;
637
+ return { name: ident, origLine: r, origCol: m.index };
638
+ }
639
+ }
640
+ return null;
641
+ }
642
+
531
643
  // ---------------------------------------------------------------------------
532
644
  // Variable collection
533
645
  // ---------------------------------------------------------------------------
@@ -577,7 +689,12 @@ export class CodeEmitter {
577
689
  }
578
690
 
579
691
  if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
580
- this.collectVarsFromLoopHead(rest[0], this.programVars);
692
+ // Don't hoist loop vars: emitForIn/emitForOf/emitForAs already
693
+ // emit `for (let x of ...)`, which is block-scoped AND gives JS's
694
+ // per-iteration binding semantics (critical for closures captured
695
+ // inside the loop). Hoisting `let x` to the surrounding scope
696
+ // would shadow that as dead code (TS6133 "declared but never
697
+ // read") without changing runtime behavior.
581
698
  rest.slice(1).forEach(item => this.collectProgramVariables(item));
582
699
  return;
583
700
  }
@@ -640,7 +757,10 @@ export class CodeEmitter {
640
757
  return;
641
758
  }
642
759
  if (head === 'for-in' || head === 'for-of' || head === 'for-as') {
643
- this.collectVarsFromLoopHead(rest[0], vars);
760
+ // See collectProgramVariables for-loop branch: loop vars are
761
+ // block-scoped via the for-header's `let`; hoisting them here
762
+ // would only produce a redundant outer `let x` flagged as
763
+ // unused.
644
764
  rest.slice(1).forEach(collect);
645
765
  return;
646
766
  }
@@ -668,20 +788,55 @@ export class CodeEmitter {
668
788
  return vars;
669
789
  }
670
790
 
791
+ // Walk a function body and collect typed local assignments. Returns a
792
+ // Map<name, typeString> for every `name:: T = value` whose target is a
793
+ // String-wrapped identifier carrying `data.type` (attached by types.js).
794
+ //
795
+ // Used by `emitBodyWithReturns` in `inlineTypes` mode to annotate the
796
+ // function-top hoist (`let a, y: boolean, b;`) so shadow-TS sees the
797
+ // intended type instead of inferring a literal from the first RHS. Stops
798
+ // at nested function boundaries — each function owns its own typed locals.
799
+ //
800
+ // Conflict policy: first annotation wins. Same-name re-annotations are
801
+ // silently ignored; mixing different types on the same local is an
802
+ // unusual pattern best surfaced by TS itself once the hoist carries the
803
+ // first annotation.
804
+ collectTypedLocals(body) {
805
+ let typed = new Map();
806
+ let walk = (sexpr) => {
807
+ if (!Array.isArray(sexpr)) return;
808
+ let [head, ...rest] = sexpr;
809
+ head = str(head);
810
+ if (Array.isArray(head)) { sexpr.forEach(walk); return; }
811
+ if (CodeEmitter.ASSIGNMENT_OPS.has(head)) {
812
+ let [target, value] = rest;
813
+ if (target instanceof String && target.type && !typed.has(str(target))) {
814
+ typed.set(str(target), target.type);
815
+ }
816
+ walk(value);
817
+ return;
818
+ }
819
+ if (head === 'def' || head === '->' || head === '=>' || head === 'effect') return;
820
+ rest.forEach(walk);
821
+ };
822
+ walk(body);
823
+ return typed;
824
+ }
825
+
671
826
  // ---------------------------------------------------------------------------
672
827
  // Main dispatch
673
828
  // ---------------------------------------------------------------------------
674
829
 
675
830
  emit(sexpr, context = 'statement') {
676
- // String object with metadata (quote, await, predicate, heregex, etc.)
831
+ // String object with metadata (quote, bang, optional, heregex, etc.)
677
832
  if (sexpr instanceof String) {
678
833
  // Dammit operator (!)
679
- if (meta(sexpr, 'await') === true) {
834
+ if (meta(sexpr, 'bang') === true) {
680
835
  return `await ${str(sexpr)}()`;
681
836
  }
682
837
 
683
838
  // Existence check (?)
684
- if (meta(sexpr, 'predicate')) {
839
+ if (meta(sexpr, 'optional')) {
685
840
  return `(${str(sexpr)} != null)`;
686
841
  }
687
842
 
@@ -737,8 +892,8 @@ export class CodeEmitter {
737
892
 
738
893
  let [head, ...rest] = sexpr;
739
894
 
740
- // Preserve await metadata before converting head to primitive
741
- let headAwaitMeta = meta(head, 'await');
895
+ // Preserve bang metadata before converting head to primitive
896
+ let headBangMeta = meta(head, 'bang');
742
897
  head = str(head);
743
898
 
744
899
  // Dispatch table
@@ -758,7 +913,7 @@ export class CodeEmitter {
758
913
  let postfix = this._tryPostfixCall(head, rest, context);
759
914
  if (postfix) return postfix;
760
915
 
761
- let needsAwait = headAwaitMeta === true;
916
+ let needsAwait = headBangMeta === true;
762
917
  let callStr = `${this.emit(head, 'value')}(${this._emitArgs(rest)})`;
763
918
  return needsAwait ? `await ${callStr}` : callStr;
764
919
  }
@@ -784,15 +939,16 @@ export class CodeEmitter {
784
939
  let postfix = this._tryPostfixCall(head, rest, context);
785
940
  if (postfix) return postfix;
786
941
 
787
- // Property access with await sigil on property
942
+ // Property access with bang sigil on property
788
943
  let needsAwait = false;
789
944
  let calleeCode;
790
- if (head[0] === '.' && meta(head[2], 'await') === true) {
945
+ if (head[0] === '.' && meta(head[2], 'bang') === true) {
791
946
  needsAwait = true;
792
947
  let [obj, prop] = head.slice(1);
793
948
  let objCode = this.emit(obj, 'value');
794
949
  let needsParens = CodeEmitter.NUMBER_LITERAL_RE.test(objCode) ||
795
- ((this.is(obj, 'object') || this.is(obj, 'await') || this.is(obj, 'yield')));
950
+ ((this.is(obj, 'object') || this.is(obj, 'await') || this.is(obj, 'yield'))) ||
951
+ /^(await|yield)\s/.test(objCode);
796
952
  let base = needsParens ? `(${objCode})` : objCode;
797
953
  calleeCode = `${base}.${str(prop)}`;
798
954
  } else {
@@ -850,7 +1006,12 @@ export class CodeEmitter {
850
1006
  let needsBlank = false;
851
1007
 
852
1008
  if (imports.length > 0) {
853
- code += imports.map(s => this.addSemicolon(s, this.emit(s, 'statement'))).join('\n');
1009
+ let importEntries = imports.map(s => {
1010
+ let generated = this.addSemicolon(s, this.emit(s, 'statement'));
1011
+ return { code: generated, loc: Array.isArray(s) ? s.loc : null, sexpr: Array.isArray(s) ? s : null };
1012
+ });
1013
+ this._importEntries = importEntries;
1014
+ code += importEntries.map(e => e.code).join('\n');
854
1015
  needsBlank = true;
855
1016
  }
856
1017
 
@@ -919,9 +1080,9 @@ export class CodeEmitter {
919
1080
 
920
1081
  if (this.usesTemplates && !skip) {
921
1082
  if (skipRT) {
922
- code += 'var { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __lis, __reconcile, __transition, __handleComponentError, __Component } = globalThis.__ripComponent;\n';
1083
+ code += 'var { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __lis, __reconcile, __transition, __handleComponentError, __gateBind, __Component } = globalThis.__ripComponent;\n';
923
1084
  } else if (typeof globalThis !== 'undefined' && globalThis.__ripComponent) {
924
- code += 'const { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __lis, __reconcile, __transition, __handleComponentError, __Component } = globalThis.__ripComponent;\n';
1085
+ code += 'const { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __lis, __reconcile, __transition, __handleComponentError, __gateBind, __Component } = globalThis.__ripComponent;\n';
925
1086
  } else {
926
1087
  code += this.getComponentRuntime();
927
1088
  }
@@ -972,19 +1133,35 @@ export class CodeEmitter {
972
1133
  return `${this.emit(left, 'value')}.repeat(${this.emit(right, 'value')})`;
973
1134
  }
974
1135
  }
975
- // Chained comparisons: (< (< a b) c) → ((a < b) && (b < c))
976
- let COMPARE_OPS = new Set(['<', '>', '<=', '>=']);
977
- if (COMPARE_OPS.has(op) && Array.isArray(left)) {
1136
+ // Chained comparisons: (< (< a b) c) → ((a < b) && (b < c)).
1137
+ // All COMPARE-level ops chain, including equality — otherwise
1138
+ // a == b < c silently compares a boolean to c. Recursing through
1139
+ // the left side handles chains of any length.
1140
+ let COMPARE_OPS = new Set(['<', '>', '<=', '>=', '==', '===', '!=', '!==']);
1141
+ if (COMPARE_OPS.has(op) && Array.isArray(left) && !left.parenthesized) {
978
1142
  let leftOp = left[0]?.valueOf?.() ?? left[0];
979
1143
  if (COMPARE_OPS.has(leftOp)) {
980
- let a = this.emit(left[1], 'value');
1144
+ let jsOp = (o) => o === '==' ? '===' : o === '!=' ? '!==' : o;
1145
+ let leftCode = this.emit(left, 'value');
981
1146
  let b = this.emit(left[2], 'value');
982
1147
  let c = this.emit(right, 'value');
983
- return `((${a} ${leftOp} ${b}) && (${b} ${op} ${c}))`;
1148
+ return `(${leftCode} && (${b} ${jsOp(op)} ${c}))`;
984
1149
  }
985
1150
  }
986
1151
  if (op === '==') op = '===';
987
1152
  if (op === '!=') op = '!==';
1153
+ // JS requires the base of ** to be an UpdateExpression: a bare unary
1154
+ // (typeof x ** 2, !a ** b, await f() ** 2) is a SyntaxError unless
1155
+ // parenthesized. Wrap unary-headed left operands that emit bare.
1156
+ if (op === '**' && Array.isArray(left)) {
1157
+ let UNARY_HEADS = new Set(['!', '~', 'typeof', 'void', 'delete', 'await', 'not', '+', '-']);
1158
+ let leftHead = left[0]?.valueOf?.() ?? left[0];
1159
+ if (UNARY_HEADS.has(leftHead)) {
1160
+ let lc = this.emit(left, 'value');
1161
+ if (!lc.startsWith('(')) lc = `(${lc})`;
1162
+ return `(${lc} ** ${this.emit(right, 'value')})`;
1163
+ }
1164
+ }
988
1165
  return `(${this.emit(left, 'value')} ${op} ${this.emit(right, 'value')})`;
989
1166
  }
990
1167
 
@@ -1042,15 +1219,7 @@ export class CodeEmitter {
1042
1219
  }
1043
1220
 
1044
1221
  // 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
- }
1222
+ this.applyVoidMarker(target, value, sexpr);
1054
1223
 
1055
1224
  // Empty destructuring — just evaluate RHS
1056
1225
  let isEmptyArr = this.is(target, 'array', 0);
@@ -1139,7 +1308,7 @@ export class CodeEmitter {
1139
1308
 
1140
1309
  // Generate target (handle reactive, sigils)
1141
1310
  let targetCode;
1142
- if (target instanceof String && meta(target, 'await') !== undefined) {
1311
+ if (target instanceof String && meta(target, 'bang') !== undefined) {
1143
1312
  targetCode = str(target);
1144
1313
  } else if (typeof target === 'string' && this.reactiveVars?.has(target)) {
1145
1314
  targetCode = `${target}.value`;
@@ -1186,8 +1355,8 @@ export class CodeEmitter {
1186
1355
  objCode.startsWith('await ') ||
1187
1356
  ((this.is(obj, 'object') || this.is(obj, 'yield')));
1188
1357
  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)`;
1358
+ if (meta(prop, 'bang') === true) return `await ${base}.${str(prop)}()`;
1359
+ if (meta(prop, 'optional')) return `(${base}.${str(prop)} != null)`;
1191
1360
  return `${base}.${str(prop)}`;
1192
1361
  }
1193
1362
 
@@ -1353,7 +1522,7 @@ export class CodeEmitter {
1353
1522
 
1354
1523
  emitDef(head, rest, context, sexpr) {
1355
1524
  let [name, params, body] = rest;
1356
- let sideEffectOnly = meta(name, 'await') === true;
1525
+ let sideEffectOnly = meta(name, 'bang') === true;
1357
1526
  let cleanName = str(name);
1358
1527
  let paramList = this.emitParamList(params);
1359
1528
  let bodyCode = this.emitFunctionBody(body, params, sideEffectOnly);
@@ -1365,8 +1534,7 @@ export class CodeEmitter {
1365
1534
  emitThinArrow(head, rest, context, sexpr) {
1366
1535
  let [params, body] = rest;
1367
1536
  if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(body)) params = ['it'];
1368
- let sideEffectOnly = this.nextFunctionIsVoid || false;
1369
- this.nextFunctionIsVoid = false;
1537
+ let sideEffectOnly = sexpr.isVoid || false;
1370
1538
  let paramList = this.emitParamList(params);
1371
1539
  let bodyCode = this.emitFunctionBody(body, params, sideEffectOnly);
1372
1540
  let isAsync = this.containsAwait(body);
@@ -1378,8 +1546,7 @@ export class CodeEmitter {
1378
1546
  emitFatArrow(head, rest, context, sexpr) {
1379
1547
  let [params, body] = rest;
1380
1548
  if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(body)) params = ['it'];
1381
- let sideEffectOnly = this.nextFunctionIsVoid || false;
1382
- this.nextFunctionIsVoid = false;
1549
+ let sideEffectOnly = sexpr.isVoid || false;
1383
1550
  let paramList = this.emitParamList(params);
1384
1551
  let isSingle = params.length === 1 && typeof params[0] === 'string' &&
1385
1552
  !paramList.includes('=') && !paramList.includes('...') &&
@@ -1461,6 +1628,12 @@ export class CodeEmitter {
1461
1628
  return `const ${str(name) ?? name} = ${this.emit(expr, 'value')}`;
1462
1629
  }
1463
1630
 
1631
+ // The component macro consumes 'gate' statements before generator dispatch,
1632
+ // so reaching this generator means the binding sits outside a component body.
1633
+ emitGate(head, rest, context, sexpr) {
1634
+ this.error(`'<~' (render-ready gate) is only valid at the top of a component body`, sexpr);
1635
+ }
1636
+
1464
1637
  emitEffect(head, rest) {
1465
1638
  let [target, body] = rest;
1466
1639
  this.usesReactivity = true;
@@ -1514,31 +1687,63 @@ export class CodeEmitter {
1514
1687
  return `(${this.unwrap(this.emit(cond, 'value'))} ? ${this.emit(then_, 'value')} : ${this.emit(else_, 'value')})`;
1515
1688
  }
1516
1689
 
1517
- emitPipe(head, rest) {
1518
- let [left, right] = rest;
1519
- let leftCode = this.emit(left, 'value');
1520
- // Detect function calls: [fn, ...args] where fn is an identifier or accessor
1521
- if (Array.isArray(right) && right.length > 1) {
1522
- let fn = right[0];
1523
- let isCall = Array.isArray(fn) || (typeof fn === 'string' && /^[a-zA-Z_$]/.test(fn));
1524
- if (isCall) {
1525
- let fnCode = this.emit(fn, 'value');
1526
- let args = right.slice(1).map(a => this.emit(a, 'value'));
1527
- return `${fnCode}(${leftCode}, ${args.join(', ')})`;
1528
- }
1529
- }
1530
- // Simple reference or property access — call with left as sole arg
1531
- return `${this.emit(right, 'value')}(${leftCode})`;
1532
- }
1533
-
1534
- emitLoop(head, rest) {
1690
+ emitLoop(head, rest, context) {
1691
+ if (context === 'value') return this.emitLoopAsValue('while (true)', rest[0]);
1535
1692
  return `while (true) ${this.emitLoopBody(rest[0])}`;
1536
1693
  }
1537
1694
 
1538
- emitLoopN(head, rest) {
1695
+ emitLoopN(head, rest, context) {
1539
1696
  let [count, body] = rest;
1540
1697
  let n = this.emit(count, 'value');
1541
- return `for (let it = 0; it < ${n}; it++) ${this.emitLoopBody(body)}`;
1698
+ let header = `for (let it = 0; it < ${n}; it++)`;
1699
+ if (context === 'value') return this.emitLoopAsValue(header, body);
1700
+ return `${header} ${this.emitLoopBody(body)}`;
1701
+ }
1702
+
1703
+ // Loops used as expressions collect each iteration's last body value into
1704
+ // an array (CoffeeScript semantics): x = (f() while c) → x = [f(), ...].
1705
+ // Mirrors emitForIn's value-context routing through comprehensions.
1706
+ emitLoopAsValue(header, body, guard) {
1707
+ let hasAwait = this.containsAwait(body) || (guard != null && this.containsAwait(guard));
1708
+ let code = this.asyncIIFEOpen(hasAwait) + '\n';
1709
+ this.indentLevel++;
1710
+ code += this.indent() + 'const result = [];\n';
1711
+ code += this.indent() + header + ' {\n';
1712
+ this.indentLevel++;
1713
+ if (guard != null) {
1714
+ code += this.indent() + `if (!(${this.unwrap(this.emit(guard, 'value'))})) continue;\n`;
1715
+ }
1716
+
1717
+ let stmts;
1718
+ if (!Array.isArray(body)) stmts = [body];
1719
+ else if (body[0] === 'block') stmts = body.slice(1);
1720
+ else if (Array.isArray(body[0])) stmts = body;
1721
+ else stmts = [body];
1722
+
1723
+ let loopStmts = ['for-in', 'for-of', 'for-as', 'while', 'loop'];
1724
+ let hasCtrl = (node) => {
1725
+ if (typeof node === 'string' && (node === 'break' || node === 'continue')) return true;
1726
+ if (!Array.isArray(node)) return false;
1727
+ if (['break', 'continue', 'return', 'throw'].includes(node[0])) return true;
1728
+ if (node[0] === 'if') return node.slice(1).some(hasCtrl);
1729
+ return node.some(hasCtrl);
1730
+ };
1731
+
1732
+ for (let i = 0; i < stmts.length; i++) {
1733
+ let s = stmts[i], isLast = i === stmts.length - 1;
1734
+ if (!isLast || hasCtrl(s) || (Array.isArray(s) && loopStmts.includes(s[0]))) {
1735
+ code += this.indent() + this.addSemicolon(s, this.emit(s, 'statement')) + '\n';
1736
+ } else {
1737
+ code += this.indent() + `result.push(${this.emit(s, 'value')});\n`;
1738
+ }
1739
+ }
1740
+
1741
+ this.indentLevel--;
1742
+ code += this.indent() + '}\n';
1743
+ code += this.indent() + 'return result;\n';
1744
+ this.indentLevel--;
1745
+ code += this.indent() + '})()';
1746
+ return code;
1542
1747
  }
1543
1748
 
1544
1749
  emitAwait(head, rest) { return `await ${this.emit(rest[0], 'value')}`; }
@@ -1777,10 +1982,11 @@ export class CodeEmitter {
1777
1982
  return code;
1778
1983
  }
1779
1984
 
1780
- emitWhile(head, rest) {
1985
+ emitWhile(head, rest, context) {
1781
1986
  let cond = rest[0], guard = rest.length === 3 ? rest[1] : null, body = rest[rest.length - 1];
1782
- let code = `while (${this.unwrap(this.emit(cond, 'value'))}) `;
1783
- return code + (guard ? this.emitLoopBodyWithGuard(body, guard) : this.emitLoopBody(body));
1987
+ let header = `while (${this.unwrap(this.emit(cond, 'value'))})`;
1988
+ if (context === 'value') return this.emitLoopAsValue(header, body, guard);
1989
+ return header + ' ' + (guard ? this.emitLoopBodyWithGuard(body, guard) : this.emitLoopBody(body));
1784
1990
  }
1785
1991
 
1786
1992
  emitRange(head, rest) {
@@ -1896,7 +2102,20 @@ export class CodeEmitter {
1896
2102
  // Symbol literals
1897
2103
  // ---------------------------------------------------------------------------
1898
2104
 
1899
- emitSymbol(head, rest) { return `Symbol.for(${JSON.stringify(rest[0])})`; }
2105
+ emitSymbol(head, rest, context, sexpr) {
2106
+ // Anchor the symbol name's source position to the generated `Symbol`
2107
+ // identifier so hover on `:foo` shows `SymbolConstructor` (the
2108
+ // generated `"foo"` is a string literal that TS has no hover for).
2109
+ // sexpr.loc points at the `:`; the name starts at col + 1.
2110
+ if (sexpr && sexpr.loc && typeof rest[0] === 'string') {
2111
+ sexpr._anchors = (sexpr._anchors || []).concat([{
2112
+ name: 'Symbol',
2113
+ origLine: sexpr.loc.r,
2114
+ origCol: sexpr.loc.c + 1,
2115
+ }]);
2116
+ }
2117
+ return `Symbol.for(${JSON.stringify(rest[0])})`;
2118
+ }
1900
2119
 
1901
2120
  // ---------------------------------------------------------------------------
1902
2121
  // Data structures
@@ -1996,8 +2215,7 @@ export class CodeEmitter {
1996
2215
  if (operator === ':' && isSimpleKey && this.is(value, '->')) {
1997
2216
  let [, mParams, mBody] = value;
1998
2217
  if ((!mParams || (Array.isArray(mParams) && mParams.length === 0)) && this.containsIt(mBody)) mParams = ['it'];
1999
- let mSideEffect = this.nextFunctionIsVoid || false;
2000
- this.nextFunctionIsVoid = false;
2218
+ let mSideEffect = value.isVoid || false;
2001
2219
  let mParamList = this.emitParamList(mParams);
2002
2220
  let mBodyCode = this.emitFunctionBody(mBody, mParams, mSideEffect);
2003
2221
  let mIsAsync = this.containsAwait(mBody);
@@ -2578,7 +2796,7 @@ export class CodeEmitter {
2578
2796
  emitImport(head, rest, context, sexpr) {
2579
2797
  if (rest.length === 1) {
2580
2798
  let importExpr = `import(${this.emit(rest[0], 'value')})`;
2581
- if (meta(sexpr[0], 'await') === true) return `(await ${importExpr})`;
2799
+ if (meta(sexpr[0], 'bang') === true) return `(await ${importExpr})`;
2582
2800
  return importExpr;
2583
2801
  }
2584
2802
  if (this.options.skipImports) return '';
@@ -2587,6 +2805,7 @@ export class CodeEmitter {
2587
2805
  let fixedSource = this.addJsExtensionAndAssertions(source);
2588
2806
  if (named[0] === '*' && named.length === 2) return `import ${def}, * as ${named[1]} from ${fixedSource}`;
2589
2807
  let names = named.map(i => Array.isArray(i) && i.length === 2 ? `${i[0]} as ${i[1]}` : i).join(', ');
2808
+ this._attachImportSpecifierAnchors(sexpr, [def, ...named.flatMap(i => Array.isArray(i) ? i : [i])]);
2590
2809
  return `import ${def}, { ${names} } from ${fixedSource}`;
2591
2810
  }
2592
2811
  let [specifier, source] = rest;
@@ -2595,11 +2814,70 @@ export class CodeEmitter {
2595
2814
  if (Array.isArray(specifier)) {
2596
2815
  if (specifier[0] === '*' && specifier.length === 2) return `import * as ${specifier[1]} from ${fixedSource}`;
2597
2816
  let names = specifier.map(i => Array.isArray(i) && i.length === 2 ? `${i[0]} as ${i[1]}` : i).join(', ');
2817
+ this._attachImportSpecifierAnchors(sexpr, specifier.flatMap(i => Array.isArray(i) ? i : [i]));
2598
2818
  return `import { ${names} } from ${fixedSource}`;
2599
2819
  }
2600
2820
  return `import ${this.emit(specifier, 'value')} from ${fixedSource}`;
2601
2821
  }
2602
2822
 
2823
+ // Attach source-map anchors for each named import specifier so hover /
2824
+ // go-to-def works on individual names in `import { foo, bar } from '...'`.
2825
+ // The parser drops .loc from specifier strings, so we recover positions
2826
+ // by scanning the source forward from the `import` keyword.
2827
+ _attachImportSpecifierAnchors(sexpr, names) {
2828
+ if (!sexpr || !sexpr.loc) return;
2829
+ const source = this.options && this.options.source;
2830
+ if (!source || !names || !names.length) return;
2831
+ const lines = this._sourceLinesCache || (this._sourceLinesCache = source.split('\n'));
2832
+ let row = sexpr.loc.r;
2833
+ let col = sexpr.loc.c;
2834
+ const anchors = [];
2835
+ for (const name of names) {
2836
+ if (typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name)) continue;
2837
+ const re = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
2838
+ let found = false;
2839
+ while (row < lines.length) {
2840
+ const line = lines[row] || '';
2841
+ // Strip trailing line comment so '# foo' doesn't match
2842
+ const codePart = line.replace(/#.*$/, '');
2843
+ re.lastIndex = 0;
2844
+ const slice = codePart.slice(col);
2845
+ const m = re.exec(slice);
2846
+ if (m) {
2847
+ const c = col + m.index;
2848
+ anchors.push({ name, origLine: row, origCol: c });
2849
+ col = c + name.length;
2850
+ found = true;
2851
+ break;
2852
+ }
2853
+ row++;
2854
+ col = 0;
2855
+ }
2856
+ if (!found) break;
2857
+ }
2858
+ if (anchors.length) sexpr._anchors = (sexpr._anchors || []).concat(anchors);
2859
+ }
2860
+
2861
+ // Propagate the void marker from a `name! = fn` LHS onto the function node.
2862
+ // The `!` suffix is recorded as `.bang === true` metadata on the target
2863
+ // identifier; when the value is a function (`->`/`=>`/`def`) the bang means
2864
+ // the function is void (no implicit return). We stamp `isVoid` directly on
2865
+ // the function node so the arrow emitters read it locally — the same way
2866
+ // `emitDef` reads `meta(name, 'bang')` off its own node. Used by assignment
2867
+ // and export declaration paths so `export name! = ->` matches the bare
2868
+ // `name! = ->` semantics. Rejects `!`/`&` sigils on non-function values,
2869
+ // exactly like a plain assignment.
2870
+ applyVoidMarker(target, value, sexpr) {
2871
+ let isFnValue = (this.is(value, '->') || this.is(value, '=>') || this.is(value, 'def'));
2872
+ if (target instanceof String && meta(target, 'bang') !== undefined && !isFnValue) {
2873
+ let sigil = meta(target, 'bang') === true ? '!' : '&';
2874
+ this.error(`Cannot use ${sigil} sigil in variable declaration '${str(target)}'`, sexpr);
2875
+ }
2876
+ if (target instanceof String && meta(target, 'bang') === true && isFnValue) {
2877
+ value.isVoid = true;
2878
+ }
2879
+ }
2880
+
2603
2881
  emitExport(head, rest) {
2604
2882
  let [decl] = rest;
2605
2883
  if (this.options.skipExports) {
@@ -2612,6 +2890,7 @@ export class CodeEmitter {
2612
2890
  this._componentTypeParams = decl[1]?.typeParams || '';
2613
2891
  }
2614
2892
  if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
2893
+ this.applyVoidMarker(decl[1], decl[2], decl);
2615
2894
  const result = `const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
2616
2895
  this._componentName = prev;
2617
2896
  this._componentTypeParams = prevTP;
@@ -2630,6 +2909,7 @@ export class CodeEmitter {
2630
2909
  this._componentTypeParams = decl[1]?.typeParams || '';
2631
2910
  }
2632
2911
  if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
2912
+ this.applyVoidMarker(decl[1], decl[2], decl);
2633
2913
  const result = `export const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
2634
2914
  this._componentName = prev;
2635
2915
  this._componentTypeParams = prevTP;
@@ -2643,10 +2923,14 @@ export class CodeEmitter {
2643
2923
  emitExportDefault(head, rest) {
2644
2924
  let [expr] = rest;
2645
2925
  if (this.options.skipExports) {
2646
- if (this.is(expr, '=')) return `const ${expr[1]} = ${this.emit(expr[2], 'value')}`;
2926
+ if (this.is(expr, '=')) {
2927
+ this.applyVoidMarker(expr[1], expr[2], expr);
2928
+ return `const ${expr[1]} = ${this.emit(expr[2], 'value')}`;
2929
+ }
2647
2930
  return this.emit(expr, 'statement');
2648
2931
  }
2649
2932
  if (this.is(expr, '=')) {
2933
+ this.applyVoidMarker(expr[1], expr[2], expr);
2650
2934
  return `const ${expr[1]} = ${this.emit(expr[2], 'value')};\nexport default ${expr[1]}`;
2651
2935
  }
2652
2936
  return `export default ${this.emit(expr, 'statement')}`;
@@ -2776,14 +3060,36 @@ export class CodeEmitter {
2776
3060
 
2777
3061
  formatParam(param) {
2778
3062
  if (typeof param === 'string') return param;
2779
- if (param instanceof String) return param.valueOf();
2780
- if (this.is(param, 'rest')) return `...${param[1]}`;
3063
+ if (param instanceof String) {
3064
+ // In `inlineTypes` mode (set by typecheck.compileForCheck), emit the
3065
+ // type annotation inline so shadow TS sees `name: T` for typed params
3066
+ // in every function-like position — top-level arrows, class methods,
3067
+ // object-literal method shorthand, nested functions, etc. The user-
3068
+ // facing `-c` output stays untouched because this flag is off by
3069
+ // default. `.type` carries the raw Rip type string (with `::`),
3070
+ // which converts to TS form by swapping `::` → `:`.
3071
+ if (this.options.inlineTypes && param.type) {
3072
+ return `${param.valueOf()}: ${param.type.replace(/::/g, ':')}`;
3073
+ }
3074
+ return param.valueOf();
3075
+ }
3076
+ if (this.is(param, 'rest')) {
3077
+ // Rest param: `...name`. When the name is a String wrapper carrying
3078
+ // a type, emit `...name: T` so shadow TS sees the rest tuple/array.
3079
+ let restName = param[1];
3080
+ if (this.options.inlineTypes && restName instanceof String && restName.type) {
3081
+ return `...${restName.valueOf()}: ${restName.type.replace(/::/g, ':')}`;
3082
+ }
3083
+ return `...${restName}`;
3084
+ }
2781
3085
  if (this.is(param, 'default')) {
2782
3086
  // `param[1]` is either a plain identifier string (e.g. `x = 5`) or a
2783
3087
  // destructuring pattern AST node (e.g. `{a, b} = {}`). Recurse via
2784
3088
  // `formatParam` so patterns emit as `{a, b}` / `[x, y]` instead of
2785
3089
  // being coerced to a string via `Array.prototype.toString`, which
2786
3090
  // produced the famous `(object,,a,a,,b,b = {})` mis-rendering.
3091
+ // The recursion also picks up any inline type annotation on the
3092
+ // name in `inlineTypes` mode, yielding `name: T = default`.
2787
3093
  return `${this.formatParam(param[1])} = ${this.emit(param[2], 'value')}`;
2788
3094
  }
2789
3095
  if (this.is(param, '.') && param[1] === 'this') return param[2];
@@ -2823,10 +3129,14 @@ export class CodeEmitter {
2823
3129
 
2824
3130
  let paramNames = new Set();
2825
3131
  let extractPN = (p) => {
2826
- if (typeof p === 'string') paramNames.add(p);
3132
+ // Unwrap String wrappers — typed params arrive as `new String('name')`
3133
+ // with `.data.type` metadata. Without unwrapping, `paramNames.has('name')`
3134
+ // misses (Set compares wrappers by identity), causing the param name to
3135
+ // be re-hoisted as a local `let`, producing duplicate-declaration errors.
3136
+ if (typeof p === 'string' || p instanceof String) paramNames.add(str(p));
2827
3137
  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]); }
3138
+ if (p[0] === 'rest' || p[0] === '...') { if (typeof p[1] === 'string' || p[1] instanceof String) paramNames.add(str(p[1])); }
3139
+ else if (p[0] === 'default') { if (typeof p[1] === 'string' || p[1] instanceof String) paramNames.add(str(p[1])); }
2830
3140
  else if (p[0] === 'array' || p[0] === 'object') this.collectVarsFromArray(p, paramNames);
2831
3141
  }
2832
3142
  };
@@ -2870,7 +3180,20 @@ export class CodeEmitter {
2870
3180
 
2871
3181
  this.indentLevel++;
2872
3182
  let code = '{\n';
2873
- if (newVars.size > 0) code += this.indent() + `let ${Array.from(newVars).sort().join(', ')};\n`;
3183
+ if (newVars.size > 0) {
3184
+ // In `inlineTypes` mode, propagate `name:: T = value` annotations from
3185
+ // body-level typed assignments onto the hoisted `let`. Without this,
3186
+ // the hoist emits `let y;` (no type), shadow-TS infers from the first
3187
+ // RHS literal (`y = true` → `y: true`), and any later `y = false`
3188
+ // fails TS2322. With this, the hoist emits `let y: boolean;` and the
3189
+ // body's `y = true` / `y = false` both check cleanly.
3190
+ let typedLocals = this.options.inlineTypes ? this.collectTypedLocals(body) : null;
3191
+ let names = Array.from(newVars).sort().map(n => {
3192
+ let t = typedLocals?.get(n);
3193
+ return t ? `${n}: ${t}` : n;
3194
+ });
3195
+ code += this.indent() + `let ${names.join(', ')};\n`;
3196
+ }
2874
3197
 
2875
3198
  let firstIsSuper = autoAssignments.length > 0 && statements.length > 0 &&
2876
3199
  Array.isArray(statements[0]) && statements[0][0] === 'super';
@@ -3707,7 +4030,7 @@ export class CodeEmitter {
3707
4030
 
3708
4031
  containsAwait(sexpr) {
3709
4032
  if (!sexpr) return false;
3710
- if (sexpr instanceof String && meta(sexpr, 'await') === true) return true;
4033
+ if (sexpr instanceof String && meta(sexpr, 'bang') === true) return true;
3711
4034
  if (typeof sexpr !== 'object') return false;
3712
4035
  if (this.is(sexpr, 'await')) return true;
3713
4036
  if (this.is(sexpr, 'for-as') && sexpr[3] === true) return true;
@@ -4166,7 +4489,13 @@ export class Compiler {
4166
4489
  // in this file at all — the exporting module strips its `export type` to
4167
4490
  // nothing, so leaving the specifier in place would cause a runtime
4168
4491
  // "module does not provide an export named X" error.
4169
- if (lexer.typeRefNames?.size > 0) {
4492
+ //
4493
+ // Skip elision in `inlineTypes` mode (set by typecheck.compileForCheck).
4494
+ // The shadow-TS output is fed to the TypeScript language service, not
4495
+ // executed — so keeping the specifiers preserves go-to-definition and
4496
+ // hover for type-only imports like `RetryConfig` that resolve to
4497
+ // `export type` declarations in the target module.
4498
+ if (lexer.typeRefNames?.size > 0 && !this.options.inlineTypes) {
4170
4499
  let usedNames = new Set();
4171
4500
  let inImport = false;
4172
4501
  for (let t of tokens) {
@@ -4308,7 +4637,7 @@ export class Compiler {
4308
4637
  // If only terminators remain (type-only source), emit types and return early
4309
4638
  if (tokens.every(t => t[0] === 'TERMINATOR')) {
4310
4639
  if (typeTokens && _typesEmitter) dts = _typesEmitter(typeTokens, ['program'], source);
4311
- return { tokens, sexpr: ['program'], code: '', dts, data: dataSection, reactiveVars: {} };
4640
+ return { tokens, sexpr: ['program'], code: '', dts, data: dataSection, reactiveVars: {}, gates: [] };
4312
4641
  }
4313
4642
 
4314
4643
  // Step 3: Parse — shim adapter wraps token values with metadata
@@ -4377,11 +4706,18 @@ export class Compiler {
4377
4706
  skipDataPart: this.options.skipDataPart,
4378
4707
  stubComponents: this.options.stubComponents,
4379
4708
  reactiveVars: this.options.reactiveVars,
4709
+ // Emit `name: T` inline on typed params so shadow TS in compileForCheck
4710
+ // sees annotations on every function-like position (top-level arrows,
4711
+ // class methods, object-literal method shorthand, nested functions).
4712
+ // Off by default — only set when producing input for the shadow TS pass.
4713
+ inlineTypes: this.options.inlineTypes,
4380
4714
  // Schema runtime mode: 'browser' / 'validate' / 'server' / 'migration'.
4381
4715
  // Default 'migration' covers the common case (CLI, server, tests) where
4382
4716
  // the user might call any schema feature including .toSQL(). The browser
4383
4717
  // bundle build script overrides to 'browser' for size reduction.
4384
4718
  schemaMode: this.options.schemaMode,
4719
+ // Opt-in: fold derived projection schemas to self-contained literals.
4720
+ foldProjections: this.options.foldProjections,
4385
4721
  sourceMap,
4386
4722
  });
4387
4723
  let code = generator.compile(sexpr);
@@ -4408,12 +4744,14 @@ export class Compiler {
4408
4744
  code += `\n//# sourceMappingURL=${this.options.filename}.js.map`;
4409
4745
  }
4410
4746
 
4411
- // Step 5: Emit .d.ts from annotated tokens + parsed s-expression
4747
+ // Step 5: Emit .d.ts from annotated tokens + parsed s-expression. The
4748
+ // generator's `_schemaBehavior` buffer (populated during codegen, shadow-TS
4749
+ // mode only) lets the type emitter infer computed/derived return types.
4412
4750
  if (typeTokens && _typesEmitter) {
4413
- dts = _typesEmitter(typeTokens, sexpr, source);
4751
+ dts = _typesEmitter(typeTokens, sexpr, source, generator._schemaBehavior, generator._schemaAnon);
4414
4752
  }
4415
4753
 
4416
- return { tokens, sexpr, code, dts, map, reverseMap, data: dataSection, reactiveVars: generator.reactiveVars };
4754
+ return { tokens, sexpr, code, dts, map, reverseMap, data: dataSection, reactiveVars: generator.reactiveVars, gates: generator._gateDecls || [] };
4417
4755
  }
4418
4756
 
4419
4757
  compileToJS(source) { return this.compile(source).code; }
@@ -4480,6 +4818,126 @@ export function emit(sexpr, options = {}) {
4480
4818
  return new CodeEmitter(options).compile(sexpr);
4481
4819
  }
4482
4820
 
4821
+ // =============================================================================
4822
+ // Client projection extraction (browser bundle boundary)
4823
+ // =============================================================================
4824
+ //
4825
+ // When a browser-bundled module imports a binding from a server-only file
4826
+ // (e.g. `import { UserView } from '../api/models.rip'`), that file can't ship
4827
+ // to the browser — it carries the model's ORM/DDL. This lifts ONLY the named
4828
+ // bindings, and only when each is a self-contained schema, into a synthetic
4829
+ // shared module the browser can compile.
4830
+ //
4831
+ // The source is compiled with projection folding ON, so a derived schema
4832
+ // (`UserView = User.pick(...)`) becomes a source-free `__schema({...})` literal.
4833
+ // A binding is shippable iff it's such a literal AND not a :model AND carries
4834
+ // no behavior (methods/computed/transforms — which compile to functions that
4835
+ // could close over server-only imports). Anything else is refused with a
4836
+ // reason, keeping server code out of the browser by construction.
4837
+ //
4838
+ // Returns { ok: true, source } where `source` is synthetic .rip the browser can
4839
+ // compile, or { ok: false, error } listing every binding that can't be shipped.
4840
+
4841
+ function skipJsString(code, i, quote) {
4842
+ i++;
4843
+ while (i < code.length && code[i] !== quote) {
4844
+ if (code[i] === '\\') i += 2; else i++;
4845
+ }
4846
+ return i + 1;
4847
+ }
4848
+
4849
+ // Locate a top-level `NAME = __schema(...)` binding in compiled JS and return
4850
+ // { text, isModel, hasBehavior } — or null when NAME isn't bound to a __schema
4851
+ // literal (an unfolded `Model.pick(x)` call, a function, a plain value, or
4852
+ // absent). Balanced extraction skips nested parens/brackets/braces and strings.
4853
+ function findSchemaLiteral(code, name) {
4854
+ // `name` is a schema identifier, but `$` is both a valid identifier char and
4855
+ // a regex metacharacter, so escape before interpolating into the pattern.
4856
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4857
+ const re = new RegExp(`(?:^|\\n)\\s*(?:export\\s+const\\s+)?${escaped}\\s*=\\s*__schema\\(`, 'g');
4858
+ const m = re.exec(code);
4859
+ if (!m) return null;
4860
+ let i = m.index + m[0].length; // just past the opening '('
4861
+ const start = i;
4862
+ let depth = 1;
4863
+ while (i < code.length && depth > 0) {
4864
+ const c = code[i];
4865
+ if (c === '"' || c === "'" || c === '`') { i = skipJsString(code, i, c); continue; }
4866
+ if (c === '(' || c === '[' || c === '{') depth++;
4867
+ else if (c === ')' || c === ']' || c === '}') depth--;
4868
+ i++;
4869
+ }
4870
+ if (depth !== 0) return null;
4871
+ const argText = code.slice(start, i - 1);
4872
+ // Behavior is detected structurally, not by scanning for the word "function":
4873
+ // a callable entry surfaces as `tag: "computed"|...`, and a field transform as
4874
+ // `transform: (function…)`. A bare `\bfunction\b` would also match a literal
4875
+ // VALUE like `kind! "function" | "class"` and wrongly refuse a plain shape.
4876
+ return {
4877
+ text: `__schema(${argText})`,
4878
+ isModel: /^\s*\{\s*kind:\s*"model"/.test(argText),
4879
+ hasBehavior: /\btransform:/.test(argText) || /tag:\s*"(?:computed|method|hook|ensure|derived)"/.test(argText),
4880
+ };
4881
+ }
4882
+
4883
+ export function extractClientProjections(source, names, { filename } = {}) {
4884
+ let compiled;
4885
+ try {
4886
+ compiled = new Compiler({
4887
+ filename,
4888
+ foldProjections: true,
4889
+ skipPreamble: true,
4890
+ skipRuntimes: true,
4891
+ skipDataPart: true,
4892
+ skipImports: true,
4893
+ }).compile(source);
4894
+ } catch (e) {
4895
+ return { ok: false, error: `failed to compile ${filename || 'module'}: ${e.message}` };
4896
+ }
4897
+ const code = compiled.code || '';
4898
+ // Every schema bound in this source — lets us tell a nested *schema-typed*
4899
+ // field (`items! OrderItem[]` → `typeName: "OrderItem"`) apart from a
4900
+ // primitive (`typeName: "string"`).
4901
+ const defined = new Set();
4902
+ for (const m of code.matchAll(/(?:^|\n)\s*(?:export\s+const\s+)?(\w+)\s*=\s*__schema\(/g)) defined.add(m[1]);
4903
+
4904
+ const lines = [];
4905
+ const errors = [];
4906
+ const where = filename ? ` from '${filename}'` : '';
4907
+ const requested = new Set(names);
4908
+ const seen = new Set();
4909
+ // Process requested names, then transitively any schema they reference by a
4910
+ // field type — otherwise the shared module ships a shape whose nested type
4911
+ // isn't registered in the browser, and that field's validation is *silently*
4912
+ // skipped. A pulled-in dependency that isn't itself shippable is a hard error.
4913
+ const queue = [...names];
4914
+ while (queue.length) {
4915
+ const name = queue.shift();
4916
+ if (seen.has(name)) continue;
4917
+ seen.add(name);
4918
+ const lit = findSchemaLiteral(code, name);
4919
+ const isDep = !requested.has(name);
4920
+ const tag = isDep ? `'${name}'${where} (a nested type of a shipped projection)` : `'${name}'${where}`;
4921
+ if (!lit) {
4922
+ errors.push(isDep
4923
+ ? `${tag} isn't a shippable schema, so the projection referencing it can't cross to the browser`
4924
+ : `${tag} isn't a shippable schema — only a schema or a folded projection (e.g. ${name} = Model.pick(...)) can cross to the browser`);
4925
+ } else if (lit.isModel) {
4926
+ errors.push(`${tag} is a :model — ship a projection like ${name}.pick(...)/.omit(...) instead of the model (which carries ORM/DDL)`);
4927
+ } else if (lit.hasBehavior) {
4928
+ errors.push(`${tag} carries behavior (methods/computed/transforms) that may close over server-only code — project it to plain fields before importing it client-side`);
4929
+ } else {
4930
+ lines.push(`export ${name} = ${lit.text}`);
4931
+ for (const tm of lit.text.matchAll(/typeName:\s*"(\w+)"/g)) {
4932
+ const dep = tm[1];
4933
+ if (defined.has(dep) && !seen.has(dep)) queue.push(dep);
4934
+ }
4935
+ }
4936
+ }
4937
+ if (errors.length) return { ok: false, error: errors.join('; ') };
4938
+ return { ok: true, source: lines.join('\n') + '\n' };
4939
+ }
4940
+
4483
4941
  export function getReactiveRuntime() {
4484
4942
  return new CodeEmitter({}).getReactiveRuntime();
4485
4943
  }