rip-lang 3.16.1 → 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 (40) hide show
  1. package/README.md +2 -3
  2. package/bin/rip +39 -8
  3. package/bin/rip-schema +175 -0
  4. package/docs/RIP-APP.md +91 -2
  5. package/docs/RIP-DUCKDB.md +64 -1
  6. package/docs/RIP-INTRO.md +4 -4
  7. package/docs/RIP-LANG.md +32 -33
  8. package/docs/RIP-SCHEMA.md +1204 -364
  9. package/docs/dist/rip.js +3245 -611
  10. package/docs/dist/rip.min.js +1161 -289
  11. package/docs/dist/rip.min.js.br +0 -0
  12. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  13. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  14. package/docs/extensions/vscode/rip/index.html +2 -1
  15. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  16. package/docs/extensions/vscode/rip/vscode-rip-0.6.0.vsix +0 -0
  17. package/docs/index.html +1 -1
  18. package/docs/ui/hljs-rip.js +1 -1
  19. package/package.json +7 -4
  20. package/src/AGENTS.md +39 -8
  21. package/src/compiler.js +220 -36
  22. package/src/components.js +315 -14
  23. package/src/dts.js +18 -3
  24. package/src/grammar/README.md +29 -170
  25. package/src/grammar/grammar.rip +17 -12
  26. package/src/grammar/solar.rip +4 -17
  27. package/src/lexer.js +24 -17
  28. package/src/parser.js +229 -229
  29. package/src/schema/dts.js +328 -54
  30. package/src/schema/loader-server.js +2 -1
  31. package/src/schema/runtime-browser-stubs.js +20 -9
  32. package/src/schema/runtime-ddl.js +161 -44
  33. package/src/schema/runtime-migrate.js +681 -0
  34. package/src/schema/runtime-orm.js +698 -54
  35. package/src/schema/runtime-validate.js +808 -24
  36. package/src/schema/runtime.generated.js +2395 -135
  37. package/src/schema/schema.js +1049 -89
  38. package/src/typecheck.js +283 -55
  39. package/src/types.js +5 -1
  40. package/src/grammar/lunar.rip +0 -2412
Binary file
@@ -32,7 +32,8 @@ cursor --install-extension ./rip-latest.vsix
32
32
 
33
33
  <h2>Versions</h2>
34
34
  <ul>
35
- <li><a href="./rip-0.5.15.vsix"><code>rip-0.5.15.vsix</code></a></li>
35
+ <li><a href="./vscode-rip-0.6.0.vsix"><code>vscode-rip-0.6.0.vsix</code></a></li>
36
+ <li><a href="./rip-0.5.15.vsix"><code>rip-0.5.15.vsix</code></a></li>
36
37
  </ul>
37
38
 
38
39
  <footer>← <a href="../../">all rip-lang extensions</a></footer>
package/docs/index.html CHANGED
@@ -634,7 +634,7 @@
634
634
  typeKeywords: ['number', 'string', 'boolean', 'void', 'any', 'never', 'unknown', 'object', 'symbol', 'bigint'],
635
635
 
636
636
  operators: [
637
- '|>', '::', ':=', '~=', '~>', '<=>', '=!', '!?', '=~', '??', '?.', '...',
637
+ '::', ':=', '~=', '~>', '<=>', '=!', '!?', '=~', '??', '?.', '...',
638
638
  '..', '**', '//', '%%', '++', '--', '&&', '||', '===', '!==', '==', '!=',
639
639
  '<=', '>=', '=>', '->', '+=', '-=', '*=', '/=', '%=', '**=', '&&=', '||=',
640
640
  '??=', '<<=', '>>=', '>>>=', '&=', '|=', '^=', '>>>', '>>', '<<',
@@ -165,7 +165,7 @@ export default function(hljs) {
165
165
 
166
166
  const OPERATORS = {
167
167
  className: 'operator',
168
- begin: /\|>|::|:=|~=|~>|<=>|\.=|=!|!\?|\?!|=~|\?\?=|\?\?|\?\.|\.\.\.|\.\.|=>|->|\*\*|\/\/|%%|===|!==|==|!=|<=|>=|&&|\|\||[+\-*\/%&|^~<>=!?]/,
168
+ begin: /::|:=|~=|~>|<=>|\.=|=!|!\?|\?!|=~|\?\?=|\?\?|\?\.|\.\.\.|\.\.|=>|->|\*\*|\/\/|%%|===|!==|==|!=|<=|>=|&&|\|\||[+\-*\/%&|^~<>=!?]/,
169
169
  relevance: 0,
170
170
  };
171
171
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.16.1",
3
+ "version": "3.16.2",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
@@ -49,14 +49,17 @@
49
49
  "serve": "bun scripts/serve.js",
50
50
  "test": "bun test/runner.js",
51
51
  "test:types": "bun test/types/runner.js",
52
- "test:schema": "bun test/schema/singleton.test.js && bun test/schema/errors.test.js && bun test/schema/modes.test.js && bun test/schema/build.test.js",
52
+ "test:schema": "bun test/schema/singleton.test.js && bun test/schema/errors.test.js && bun test/schema/modes.test.js && bun test/schema/build.test.js && bun test/schema/extract.test.js && bun test/schema/infer.test.js",
53
53
  "test:schema-fresh": "bun scripts/build-schema-runtime.js --check",
54
+ "test:docs": "bun scripts/doctest.js",
54
55
  "build:schema-runtime": "bun scripts/build-schema-runtime.js",
55
56
  "test:bundle": "bun test/bundle.test.js",
57
+ "test:loader": "bun test/loader-reexport.test.js",
56
58
  "test:graph": "bun scripts/check-bundle-graph.js",
57
59
  "test:server": "./bin/rip packages/server/tests/runner.rip",
58
60
  "test:time": "bun run --cwd packages/time test",
59
- "test:all": "bun run test && bun run test:types && bun run test:schema && bun run test:schema-fresh && bun run test:graph && bun run test:bundle && bun run test:server && bun run test:time",
61
+ "test:validate": "./bin/rip packages/validate/test.rip",
62
+ "test:all": "bun run test && bun run test:types && bun run test:schema && bun run test:schema-fresh && bun run test:docs && bun run test:loader && bun run test:graph && bun run test:bundle && bun run test:server && bun run test:time && bun run test:validate",
60
63
  "test:ui": "bun run --cwd packages/ui test:e2e",
61
64
  "test:ui:chromium": "bun run --cwd packages/ui test:e2e:chromium",
62
65
  "test:ui:axe": "bun run --cwd packages/ui test:e2e:axe",
@@ -90,7 +93,7 @@
90
93
  "homepage": "https://github.com/shreeve/rip-lang#readme",
91
94
  "author": "Steve Shreeve <steve.shreeve@gmail.com>",
92
95
  "license": "MIT",
93
- "devDependencies": {
96
+ "dependencies": {
94
97
  "@types/bun": "1.3.14",
95
98
  "typescript": "catalog:"
96
99
  }
package/src/AGENTS.md CHANGED
@@ -27,6 +27,7 @@ The browser bundle (`docs/dist/rip.min.js`) is built from `src/browser.js` plus
27
27
  | `src/schema/runtime-db-naming.js` | source for `db-naming` fragment (server + migration) |
28
28
  | `src/schema/runtime-orm.js` | source for `orm` fragment (server + migration) |
29
29
  | `src/schema/runtime-ddl.js` | source for `ddl` fragment (migration only) |
30
+ | `src/schema/runtime-migrate.js` | source for `migrate` fragment (migration only) — introspection, differ, status/make/migrate |
30
31
  | `src/schema/runtime-browser-stubs.js` | source for `browser-stubs` fragment (browser only) |
31
32
  | `src/types.js` | yes | only `installTypeSupport(Lexer)` — token-stream type stripper |
32
33
  | `src/error.js` | yes | runtime error formatting |
@@ -40,6 +41,8 @@ The browser bundle (`docs/dist/rip.min.js`) is built from `src/browser.js` plus
40
41
 
41
42
  The forbidden list in `scripts/check-bundle-graph.js` enforces this. If a code change would put a forbidden module on the browser graph, `bun run build` aborts before the bundler runs.
42
43
 
44
+ **Decision record — "core Rip" vs "type-enabled Rip" (June 2026):** this module graph *is* the split. Tier 0 is the erasure core (everything reachable from `src/browser.js` — types stripped, never checked); tier 1 adds `.d.ts` emission (`dts.js`, registered via `setTypesEmitter`); tier 2 adds type checking (`typecheck.js` + the TypeScript service, loaded only by `rip check`). A package-level split (`@rip-lang/core` + a typed variant) was considered and rejected: it would add publishing and versioning friction to express a boundary the import graph and the bundle-graph guard already enforce mechanically. Don't re-propose the package split; if the boundary needs strengthening, extend the forbidden list or route new type-aware behavior through a registration hook instead.
45
+
43
46
  ### Registration-hook pattern (`setTypesEmitter`, `setSchemaRuntimeProvider`)
44
47
 
45
48
  The same pattern is used twice — once for `.d.ts` emission, once for the schema runtime body. Both make the bundler's tree-shaker keep CLI/server-only code out of the browser bundle.
@@ -72,7 +75,7 @@ The mode matrix exposed by `getSchemaRuntime({ mode })`:
72
75
  | `validate` | VALIDATE | isomorphic validate-only contexts |
73
76
  | `browser` | VALIDATE + BROWSER_STUBS | the `<script type="text/rip">` runtime |
74
77
  | `server` | VALIDATE + DB_NAMING + ORM | `@rip-lang/server` and friends |
75
- | `migration` | VALIDATE + DB_NAMING + ORM + DDL | CLI / migration tool / tests (default) |
78
+ | `migration` | VALIDATE + DB_NAMING + ORM + DDL + MIGRATE | CLI / migration tool / tests (default) |
76
79
 
77
80
  Edits to `src/schema/runtime-*.js` require running `bun run build:schema-runtime` to regenerate `runtime.generated.js`. CI fails (`bun run test:schema-fresh`) if the generated file is stale.
78
81
 
@@ -236,6 +239,30 @@ Complete node reference:
236
239
  ['render', body]
237
240
  ```
238
241
 
242
+ ## Rewriter Pipeline (canonical order)
243
+
244
+ The single source of truth is `rewrite()` in `src/lexer.js`. Current order, with
245
+ the passes contributed by sidecar files marked:
246
+
247
+ 1. `removeLeadingNewlines`
248
+ 2. `rewriteDottedPicks` — must run before `rewriteMapLiterals` so pick bodies see raw `{` `}` for depth counting
249
+ 3. `rewriteMapLiterals`
250
+ 4. `closeMergeAssignments`
251
+ 5. `closeOpenCalls`
252
+ 6. `closeOpenIndexes`
253
+ 7. `rewriteTypes` — installed by `types.js`; must run before `normalizeLines`, otherwise a type-arrow `=>` inside `(...) => T` is treated as a single-liner function and wrapped in spurious INDENT/OUTDENT, derailing the type collector
254
+ 8. `normalizeLines`
255
+ 9. `rewriteRender?.()` — installed by `components.js` (optional sidecar)
256
+ 10. `rewriteSchema?.()` — installed by `schema/schema.js` (optional sidecar)
257
+ 11. `tagPostfixConditionals`
258
+ 12. `rewriteTaggedTemplates`
259
+ 13. `addImplicitBracesAndParens`
260
+ 14. `addImplicitCallCommas`
261
+
262
+ When pass order changes in `rewrite()`, update this list — other sections of
263
+ this file describe pass positions relative to their neighbors and assume this
264
+ listing is accurate.
265
+
239
266
  ## Lexer Token Format
240
267
 
241
268
  Tokens are `[tag, val]` arrays with extra properties:
@@ -356,7 +383,7 @@ The component system is a compiler sidecar. `installComponentSupport(CodeEmitter
356
383
 
357
384
  ### Render Rewriter
358
385
 
359
- `rewriteRender()` runs after `normalizeLines` and before `tagPostfixConditionals`.
386
+ `rewriteRender()` runs after `normalizeLines` and before `rewriteSchema()` (see "Rewriter Pipeline" above for the full order).
360
387
 
361
388
  Inside `render` blocks it rewrites template syntax into function-call syntax:
362
389
 
@@ -653,7 +680,7 @@ enum Status
653
680
  Type emission is split across two files by execution context:
654
681
 
655
682
  - `types.js` (browser-side, ~21 KB) — `installTypeSupport(Lexer)` adds `rewriteTypes()` to strip type annotations from the token stream so user-typed Rip parses. This is the only thing the browser needs from type machinery.
656
- - `dts.js` (CLI/LSP only, ~38 KB) — `emitTypes(tokens, sexpr, source)` generates `.d.ts`, plus `tsType`, `emitComponentTypes`, and the intrinsic declaration tables (`INTRINSIC_TYPE_DECLS`, `SIGNAL_*`, `COMPUTED_*`, `EFFECT_*`, etc.). Registers itself with the compiler at module load via `setTypesEmitter()`.
683
+ - `dts.js` (CLI/LSP only, ~38 KB) — `emitTypes(tokens, sexpr, source, schemaBehavior)` generates `.d.ts`, plus `tsType`, `emitComponentTypes`, and the intrinsic declaration tables (`INTRINSIC_TYPE_DECLS`, `SIGNAL_*`, `COMPUTED_*`, `EFFECT_*`, etc.). Registers itself with the compiler at module load via `setTypesEmitter()`. `schemaBehavior` (the generator's per-compile buffer of compiled `~>`/`!>` bodies, populated only in `inlineTypes`/shadow mode) drives computed/derived return-type inference.
657
684
 
658
685
  `emitEnum` (runtime JS for `enum` blocks) lives in `compiler.js` next to the rest of the codegen dispatch — it's not type machinery, it's real runtime emission.
659
686
 
@@ -700,8 +727,10 @@ several files by execution context:
700
727
  ### Lexer path
701
728
 
702
729
  - `installSchemaSupport(Lexer, CodeEmitter)` adds `rewriteSchema()` to the
703
- Lexer prototype. It runs between `rewriteRender()` and `rewriteTypes()` in
704
- the rewriter pipeline.
730
+ Lexer prototype. It runs immediately after `rewriteRender()` and before
731
+ `tagPostfixConditionals()` in the rewriter pipeline (see "Rewriter
732
+ Pipeline" earlier in this file for the full order; note `rewriteTypes()`
733
+ is not adjacent — it runs much earlier, before `normalizeLines()`).
705
734
  - `rewriteSchema()` detects a contextual `schema` identifier at expression-
706
735
  start positions followed by either a `:kind` SYMBOL or a direct INDENT.
707
736
  The matching INDENT...OUTDENT range is parsed by a schema-specific
@@ -798,9 +827,11 @@ signatures both enforce this.
798
827
 
799
828
  ### Shadow TS
800
829
 
801
- `emitSchemaTypes(sexpr, lines)` walks the parsed s-expression for named
802
- schema declarations, emits mixins first so intersections resolve, then
803
- emits type aliases and `declare const` per kind:
830
+ `emitSchemaTypes(sexpr, lines, schemaBehavior)` walks the parsed s-expression
831
+ for named schema declarations, emits mixins first so intersections resolve, then
832
+ emits type aliases and `declare const` per kind (`schemaBehavior`, when set in
833
+ shadow mode, anchors computed/derived members to
834
+ `ReturnType<typeof __<Name>__behavior.field>`):
804
835
 
805
836
  - `:input` → `Schema<ValueType, ValueType>`
806
837
  - `:shape` → `Schema<ShapeInstance, ShapeData>` (or `Schema<Data, Data>` when
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
 
@@ -1075,9 +1080,9 @@ export class CodeEmitter {
1075
1080
 
1076
1081
  if (this.usesTemplates && !skip) {
1077
1082
  if (skipRT) {
1078
- 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';
1079
1084
  } else if (typeof globalThis !== 'undefined' && globalThis.__ripComponent) {
1080
- 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';
1081
1086
  } else {
1082
1087
  code += this.getComponentRuntime();
1083
1088
  }
@@ -1128,19 +1133,35 @@ export class CodeEmitter {
1128
1133
  return `${this.emit(left, 'value')}.repeat(${this.emit(right, 'value')})`;
1129
1134
  }
1130
1135
  }
1131
- // Chained comparisons: (< (< a b) c) → ((a < b) && (b < c))
1132
- let COMPARE_OPS = new Set(['<', '>', '<=', '>=']);
1133
- 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) {
1134
1142
  let leftOp = left[0]?.valueOf?.() ?? left[0];
1135
1143
  if (COMPARE_OPS.has(leftOp)) {
1136
- let a = this.emit(left[1], 'value');
1144
+ let jsOp = (o) => o === '==' ? '===' : o === '!=' ? '!==' : o;
1145
+ let leftCode = this.emit(left, 'value');
1137
1146
  let b = this.emit(left[2], 'value');
1138
1147
  let c = this.emit(right, 'value');
1139
- return `((${a} ${leftOp} ${b}) && (${b} ${op} ${c}))`;
1148
+ return `(${leftCode} && (${b} ${jsOp(op)} ${c}))`;
1140
1149
  }
1141
1150
  }
1142
1151
  if (op === '==') op = '===';
1143
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
+ }
1144
1165
  return `(${this.emit(left, 'value')} ${op} ${this.emit(right, 'value')})`;
1145
1166
  }
1146
1167
 
@@ -1607,6 +1628,12 @@ export class CodeEmitter {
1607
1628
  return `const ${str(name) ?? name} = ${this.emit(expr, 'value')}`;
1608
1629
  }
1609
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
+
1610
1637
  emitEffect(head, rest) {
1611
1638
  let [target, body] = rest;
1612
1639
  this.usesReactivity = true;
@@ -1660,31 +1687,63 @@ export class CodeEmitter {
1660
1687
  return `(${this.unwrap(this.emit(cond, 'value'))} ? ${this.emit(then_, 'value')} : ${this.emit(else_, 'value')})`;
1661
1688
  }
1662
1689
 
1663
- emitPipe(head, rest) {
1664
- let [left, right] = rest;
1665
- let leftCode = this.emit(left, 'value');
1666
- // Detect function calls: [fn, ...args] where fn is an identifier or accessor
1667
- if (Array.isArray(right) && right.length > 1) {
1668
- let fn = right[0];
1669
- let isCall = Array.isArray(fn) || (typeof fn === 'string' && /^[a-zA-Z_$]/.test(fn));
1670
- if (isCall) {
1671
- let fnCode = this.emit(fn, 'value');
1672
- let args = right.slice(1).map(a => this.emit(a, 'value'));
1673
- return `${fnCode}(${leftCode}, ${args.join(', ')})`;
1674
- }
1675
- }
1676
- // Simple reference or property access — call with left as sole arg
1677
- return `${this.emit(right, 'value')}(${leftCode})`;
1678
- }
1679
-
1680
- emitLoop(head, rest) {
1690
+ emitLoop(head, rest, context) {
1691
+ if (context === 'value') return this.emitLoopAsValue('while (true)', rest[0]);
1681
1692
  return `while (true) ${this.emitLoopBody(rest[0])}`;
1682
1693
  }
1683
1694
 
1684
- emitLoopN(head, rest) {
1695
+ emitLoopN(head, rest, context) {
1685
1696
  let [count, body] = rest;
1686
1697
  let n = this.emit(count, 'value');
1687
- 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;
1688
1747
  }
1689
1748
 
1690
1749
  emitAwait(head, rest) { return `await ${this.emit(rest[0], 'value')}`; }
@@ -1923,10 +1982,11 @@ export class CodeEmitter {
1923
1982
  return code;
1924
1983
  }
1925
1984
 
1926
- emitWhile(head, rest) {
1985
+ emitWhile(head, rest, context) {
1927
1986
  let cond = rest[0], guard = rest.length === 3 ? rest[1] : null, body = rest[rest.length - 1];
1928
- let code = `while (${this.unwrap(this.emit(cond, 'value'))}) `;
1929
- 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));
1930
1990
  }
1931
1991
 
1932
1992
  emitRange(head, rest) {
@@ -4577,7 +4637,7 @@ export class Compiler {
4577
4637
  // If only terminators remain (type-only source), emit types and return early
4578
4638
  if (tokens.every(t => t[0] === 'TERMINATOR')) {
4579
4639
  if (typeTokens && _typesEmitter) dts = _typesEmitter(typeTokens, ['program'], source);
4580
- return { tokens, sexpr: ['program'], code: '', dts, data: dataSection, reactiveVars: {} };
4640
+ return { tokens, sexpr: ['program'], code: '', dts, data: dataSection, reactiveVars: {}, gates: [] };
4581
4641
  }
4582
4642
 
4583
4643
  // Step 3: Parse — shim adapter wraps token values with metadata
@@ -4656,6 +4716,8 @@ export class Compiler {
4656
4716
  // the user might call any schema feature including .toSQL(). The browser
4657
4717
  // bundle build script overrides to 'browser' for size reduction.
4658
4718
  schemaMode: this.options.schemaMode,
4719
+ // Opt-in: fold derived projection schemas to self-contained literals.
4720
+ foldProjections: this.options.foldProjections,
4659
4721
  sourceMap,
4660
4722
  });
4661
4723
  let code = generator.compile(sexpr);
@@ -4682,12 +4744,14 @@ export class Compiler {
4682
4744
  code += `\n//# sourceMappingURL=${this.options.filename}.js.map`;
4683
4745
  }
4684
4746
 
4685
- // 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.
4686
4750
  if (typeTokens && _typesEmitter) {
4687
- dts = _typesEmitter(typeTokens, sexpr, source);
4751
+ dts = _typesEmitter(typeTokens, sexpr, source, generator._schemaBehavior, generator._schemaAnon);
4688
4752
  }
4689
4753
 
4690
- 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 || [] };
4691
4755
  }
4692
4756
 
4693
4757
  compileToJS(source) { return this.compile(source).code; }
@@ -4754,6 +4818,126 @@ export function emit(sexpr, options = {}) {
4754
4818
  return new CodeEmitter(options).compile(sexpr);
4755
4819
  }
4756
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
+
4757
4941
  export function getReactiveRuntime() {
4758
4942
  return new CodeEmitter({}).getReactiveRuntime();
4759
4943
  }