rip-lang 3.15.4 → 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.
Files changed (112) hide show
  1. package/README.md +6 -4
  2. package/bin/rip +167 -12
  3. package/docs/AGENTS.md +1 -1
  4. package/docs/RIP-APP.md +808 -0
  5. package/docs/RIP-DUCKDB.md +477 -0
  6. package/docs/RIP-INTRO.md +396 -0
  7. package/docs/RIP-LANG.md +59 -5
  8. package/docs/RIP-SCHEMA.md +191 -8
  9. package/docs/RIP-TYPES.md +74 -103
  10. package/docs/demo/README.md +4 -3
  11. package/docs/dist/rip.js +3627 -1470
  12. package/docs/dist/rip.min.js +671 -244
  13. package/docs/dist/rip.min.js.br +0 -0
  14. package/docs/example/index.json +7 -7
  15. package/docs/example/index.json.br +0 -0
  16. package/docs/extensions/duckdb/manifest.json +1 -1
  17. package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
  18. package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
  19. package/docs/extensions/vscode/print/index.html +2 -1
  20. package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
  21. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  22. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  23. package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
  24. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  25. package/docs/ui/bundle.json +61 -0
  26. package/docs/ui/bundle.json.br +0 -0
  27. package/docs/ui/hljs-rip.js +0 -7
  28. package/docs/ui/index.css +66 -23
  29. package/docs/ui/index.html +6 -6
  30. package/package.json +9 -3
  31. package/rip-loader.js +64 -2
  32. package/src/AGENTS.md +63 -36
  33. package/src/browser.js +96 -14
  34. package/src/compiler.js +960 -143
  35. package/src/components.js +794 -88
  36. package/src/{types-emit.js → dts.js} +181 -71
  37. package/src/grammar/README.md +1 -1
  38. package/src/grammar/grammar.rip +111 -97
  39. package/src/lexer.js +132 -18
  40. package/src/parser.js +203 -205
  41. package/src/repl.js +74 -6
  42. package/src/schema/runtime-orm.js +168 -4
  43. package/src/schema/runtime-validate.js +146 -2
  44. package/src/schema/runtime.generated.js +314 -6
  45. package/src/schema/schema.js +5 -5
  46. package/src/sourcemaps.js +277 -1
  47. package/src/stdlib.js +253 -0
  48. package/src/typecheck.js +2023 -106
  49. package/src/types.js +127 -7
  50. package/docs/ui/accordion.rip +0 -103
  51. package/docs/ui/alert-dialog.rip +0 -53
  52. package/docs/ui/autocomplete.rip +0 -115
  53. package/docs/ui/avatar.rip +0 -37
  54. package/docs/ui/badge.rip +0 -15
  55. package/docs/ui/breadcrumb.rip +0 -47
  56. package/docs/ui/button-group.rip +0 -26
  57. package/docs/ui/button.rip +0 -23
  58. package/docs/ui/card.rip +0 -25
  59. package/docs/ui/carousel.rip +0 -110
  60. package/docs/ui/checkbox-group.rip +0 -61
  61. package/docs/ui/checkbox.rip +0 -33
  62. package/docs/ui/collapsible.rip +0 -50
  63. package/docs/ui/combobox.rip +0 -130
  64. package/docs/ui/context-menu.rip +0 -88
  65. package/docs/ui/date-picker.rip +0 -206
  66. package/docs/ui/dialog.rip +0 -60
  67. package/docs/ui/drawer.rip +0 -58
  68. package/docs/ui/editable-value.rip +0 -82
  69. package/docs/ui/field.rip +0 -53
  70. package/docs/ui/fieldset.rip +0 -22
  71. package/docs/ui/form.rip +0 -39
  72. package/docs/ui/grid.rip +0 -901
  73. package/docs/ui/input-group.rip +0 -28
  74. package/docs/ui/input.rip +0 -36
  75. package/docs/ui/label.rip +0 -16
  76. package/docs/ui/menu.rip +0 -134
  77. package/docs/ui/menubar.rip +0 -151
  78. package/docs/ui/meter.rip +0 -36
  79. package/docs/ui/multi-select.rip +0 -203
  80. package/docs/ui/native-select.rip +0 -33
  81. package/docs/ui/nav-menu.rip +0 -126
  82. package/docs/ui/number-field.rip +0 -162
  83. package/docs/ui/otp-field.rip +0 -89
  84. package/docs/ui/pagination.rip +0 -123
  85. package/docs/ui/popover.rip +0 -93
  86. package/docs/ui/preview-card.rip +0 -75
  87. package/docs/ui/progress.rip +0 -25
  88. package/docs/ui/radio-group.rip +0 -57
  89. package/docs/ui/resizable.rip +0 -123
  90. package/docs/ui/scroll-area.rip +0 -145
  91. package/docs/ui/select.rip +0 -151
  92. package/docs/ui/separator.rip +0 -17
  93. package/docs/ui/skeleton.rip +0 -22
  94. package/docs/ui/slider.rip +0 -165
  95. package/docs/ui/spinner.rip +0 -17
  96. package/docs/ui/table.rip +0 -27
  97. package/docs/ui/tabs.rip +0 -113
  98. package/docs/ui/textarea.rip +0 -48
  99. package/docs/ui/toast.rip +0 -87
  100. package/docs/ui/toggle-group.rip +0 -71
  101. package/docs/ui/toggle.rip +0 -24
  102. package/docs/ui/toolbar.rip +0 -38
  103. package/docs/ui/tooltip.rip +0 -85
  104. package/src/app.rip +0 -1571
  105. package/src/sourcemap-merge.js +0 -287
  106. /package/docs/demo/{components → routes}/_layout.rip +0 -0
  107. /package/docs/demo/{components → routes}/about.rip +0 -0
  108. /package/docs/demo/{components → routes}/card.rip +0 -0
  109. /package/docs/demo/{components → routes}/counter.rip +0 -0
  110. /package/docs/demo/{components → routes}/index.rip +0 -0
  111. /package/docs/demo/{components → routes}/todos.rip +0 -0
  112. /package/src/schema/{dts-emit.js → dts.js} +0 -0
@@ -1,9 +1,12 @@
1
- import { SCHEMA_INTRINSIC_DECLS, emitSchemaTypes } from "./schema/dts-emit.js";
1
+ import { SCHEMA_INTRINSIC_DECLS, emitSchemaTypes } from "./schema/dts.js";
2
2
  import { setTypesEmitter } from "./compiler.js";
3
3
 
4
4
  // Type System — .d.ts emission for Rip (CLI / typecheck only).
5
5
  //
6
- // This module is a CLI/editor-only sidecar:
6
+ // This module is the type system's sibling to src/schema/dts.js
7
+ // both are CLI/editor-only `.d.ts` emitters for their respective
8
+ // subsystem. The `dts` name signals "compile-time .d.ts emitter,
9
+ // never reachable from the browser bundle."
7
10
  //
8
11
  // emitTypes(tokens, sexpr, source) — generates .d.ts from annotated
9
12
  // tokens and the parsed s-expression tree.
@@ -31,12 +34,13 @@ export const INTRINSIC_TYPE_DECLS = [
31
34
  "type __RipBrowserElement = Omit<HTMLElement, 'querySelector' | 'querySelectorAll' | 'closest' | 'setAttribute' | 'hidden'> & { hidden: boolean | 'until-found'; setAttribute(qualifiedName: string, value: any): void; querySelector(selectors: string): __RipBrowserElement | null; querySelectorAll(selectors: string): NodeListOf<__RipBrowserElement>; closest(selectors: string): __RipBrowserElement | null; };",
32
35
  "type __RipDomEl<K extends __RipTag> = Omit<__RipElementMap[K], 'querySelector' | 'querySelectorAll' | 'closest' | 'setAttribute' | 'hidden'> & __RipBrowserElement;",
33
36
  "type __RipAttrKeys<T> = { [K in keyof T]-?: K extends 'style' | 'classList' | 'className' | 'nodeValue' | 'textContent' | 'innerHTML' | 'innerText' | 'outerHTML' | 'outerText' | 'scrollLeft' | 'scrollTop' ? never : K extends `on${string}` | `aria${string}Element` | `aria${string}Elements` ? never : T[K] extends (...args: any[]) => any ? never : (<V>() => V extends Pick<T, K> ? 1 : 2) extends (<V>() => V extends { -readonly [P in K]: T[P] } ? 1 : 2) ? K : never }[keyof T] & string;",
34
- 'type __RipEvents = { [K in keyof HTMLElementEventMap as `@${K}`]?: ((event: HTMLElementEventMap[K]) => void) | null };',
35
- 'type __RipClassValue = string | boolean | null | undefined | Record<string, boolean> | __RipClassValue[];',
36
- 'type __RipProps<K extends __RipTag> = { [P in __RipAttrKeys<__RipElementMap[K]>]?: __RipElementMap[K][P] } & __RipEvents & { ref?: string; class?: __RipClassValue | __RipClassValue[]; style?: string; [k: `data-${string}`]: any; [k: `aria-${string}`]: any };',
37
+ "type __RipEvents<K extends __RipTag> = { [E in keyof HTMLElementEventMap as `@${E}`]?: ((event: RipEvent<HTMLElementEventMap[E], __RipElementMap[K]>) => void) | null };",
38
+ 'type RipEvent<E extends Event, T extends EventTarget> = E & { readonly target: T; readonly currentTarget: T };',
39
+ 'type __RipClassValue = string | boolean | null | undefined | Record<string, boolean | null | undefined> | __RipClassValue[];',
40
+ "type __RipProps<K extends __RipTag> = { [P in __RipAttrKeys<__RipElementMap[K]>]?: __RipElementMap[K][P] } & __RipEvents<K> & { ref?: string; class?: __RipClassValue | __RipClassValue[]; style?: string; [k: `data-${string}`]: any; [k: `aria-${string}`]: any };",
37
41
  ];
38
42
 
39
- export const INTRINSIC_FN_DECL = 'declare function __ripEl<K extends __RipTag>(tag: K, props?: __RipProps<K>): void;';
43
+ export const INTRINSIC_FN_DECL = 'declare function __ripEl<K extends __RipTag>(tag: K, props?: __RipProps<K>): void;\ndeclare function __ripRoute<const T extends string>(s: T): T;';
40
44
 
41
45
  export const ARIA_TYPE_DECLS = [
42
46
  'type __RipAriaNavHandlers = { next?: () => void; prev?: () => void; first?: () => void; last?: () => void; select?: () => void; dismiss?: () => void; tab?: () => void; char?: () => void; };',
@@ -63,6 +67,17 @@ export const SIGNAL_FN = 'declare function __state<T>(value: T | Signal<T>): Sig
63
67
  export const COMPUTED_INTERFACE = 'interface Computed<T> { readonly value: T; read(): T; lock(): Computed<T>; free(): Computed<T>; kill(): T; }';
64
68
  export const COMPUTED_FN = 'declare function __computed<T>(fn: () => T): Computed<T>;';
65
69
  export const EFFECT_FN = 'declare function __effect(fn: () => void | (() => void)): () => void;';
70
+ export const BATCH_FN = 'declare function __batch<T>(fn: () => T): T;';
71
+
72
+ // Names destructured from `globalThis.__rip` in the source. The DTS
73
+ // preamble and the post-compile `declare function` injection both need
74
+ // to skip auto-declaring these names — the explicit binding shadows the
75
+ // global and would otherwise trip TS2630.
76
+ export function ripDestructuredNames(source) {
77
+ if (typeof source !== 'string') return new Set();
78
+ const inside = (source.match(/\{\s*([^}]*?)\s*\}\s*=\s*globalThis\.__rip\b/) || [])[1] || '';
79
+ return new Set(inside.split(',').map(s => s.trim().split(/[:\s]/)[0]).filter(Boolean));
80
+ }
66
81
 
67
82
  // ============================================================================
68
83
  // emitTypes — generate .d.ts from annotated tokens + s-expression tree
@@ -74,10 +89,13 @@ export function emitTypes(tokens, sexpr = null, source = '') {
74
89
  let indentStr = ' ';
75
90
  let indent = () => indentStr.repeat(indentLevel);
76
91
  let inClass = false;
92
+ let inSubclass = false; // Tracks whether the active class extends another
77
93
  let classFields = new Set(); // Track emitted field names to avoid duplicates
78
94
  let usesSignal = false;
79
95
  let usesComputed = false;
96
+ let usesBatch = false;
80
97
  let usesRipIntrinsicProps = false;
98
+ const explicitlyBound = ripDestructuredNames(source);
81
99
  const sourceLines = typeof source === 'string' ? source.split('\n') : [];
82
100
 
83
101
  // Pre-scan: detect reactive operators regardless of type annotations.
@@ -87,6 +105,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
87
105
  const tag = tokens[i][0];
88
106
  if (tag === 'REACTIVE_ASSIGN') usesSignal = true;
89
107
  else if (tag === 'COMPUTED_ASSIGN') usesComputed = true;
108
+ else if (tag === 'IDENTIFIER' && tokens[i][1] === '__batch') usesBatch = true;
90
109
  }
91
110
 
92
111
  // Format { prop; prop } into multi-line block. Only applies when the
@@ -190,7 +209,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
190
209
  let type = tokens[j].data?.type;
191
210
  if (type) hasAnyType = true;
192
211
  let hasDefault = tokens[j + 1]?.[0] === '=';
193
- props.push({ kind: 'rename', propName, localName, type: type ? expandSuffixes(type) : null, hasDefault });
212
+ props.push({ kind: 'rename', propName, localName, type: type ? tsType(type) : null, hasDefault });
194
213
  j++;
195
214
  if (hasDefault) j = skipDefault(tokens, j);
196
215
  }
@@ -203,7 +222,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
203
222
  let type = tokens[j].data?.type;
204
223
  if (type) hasAnyType = true;
205
224
  let hasDefault = tokens[j + 1]?.[0] === '=';
206
- props.push({ kind: 'simple', propName: name, type: type ? expandSuffixes(type) : null, hasDefault });
225
+ props.push({ kind: 'simple', propName: name, type: type ? tsType(type) : null, hasDefault });
207
226
  j++;
208
227
  if (hasDefault) j = skipDefault(tokens, j);
209
228
  continue;
@@ -254,7 +273,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
254
273
  let name = tokens[j][1];
255
274
  let type = tokens[j].data?.type;
256
275
  names.push(name);
257
- elemTypes.push(type ? expandSuffixes(type) : null);
276
+ elemTypes.push(type ? tsType(type) : null);
258
277
  if (type) hasAnyType = true;
259
278
  }
260
279
  j++;
@@ -268,7 +287,13 @@ export function emitTypes(tokens, sexpr = null, source = '') {
268
287
  };
269
288
 
270
289
  // Collect function parameters (handles simple, destructured, rest, defaults)
271
- let collectParams = (tokens, startIdx) => {
290
+ //
291
+ // `subclassConstructor` mirrors the compiler's @-shorthand renaming:
292
+ // when a constructor lives inside `class Foo extends Bar`, the compiler
293
+ // rewrites `@name` params to `_name` so the assignment `this.name =
294
+ // _name` can run AFTER `super(...)`. The shadow TS must use the same
295
+ // `_name` binding or the body's `_name` references will be unresolved.
296
+ let collectParams = (tokens, startIdx, subclassConstructor = false) => {
272
297
  let params = [];
273
298
  let fields = []; // Track @param:: type for class field emission
274
299
  let j = startIdx;
@@ -296,8 +321,9 @@ export function emitTypes(tokens, sexpr = null, source = '') {
296
321
  if (tokens[j]?.[0] === 'PROPERTY' || tokens[j]?.[0] === 'IDENTIFIER') {
297
322
  let name = tokens[j][1];
298
323
  let type = tokens[j].data?.type;
299
- params.push(type ? `${name}: ${expandSuffixes(type)}` : name);
300
- if (type) fields.push({ name, type: expandSuffixes(type) });
324
+ let paramName = subclassConstructor ? `_${name}` : name;
325
+ params.push(type ? `${paramName}: ${tsType(type)}` : paramName);
326
+ if (type) fields.push({ name, type: tsType(type) });
301
327
  j++;
302
328
  }
303
329
  continue;
@@ -309,7 +335,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
309
335
  if (tokens[j]?.[0] === 'IDENTIFIER') {
310
336
  let name = tokens[j][1];
311
337
  let type = tokens[j].data?.type;
312
- params.push(type ? `...${name}: ${expandSuffixes(type)}` : `...${name}: any[]`);
338
+ params.push(type ? `...${name}: ${tsType(type)}` : `...${name}: any[]`);
313
339
  j++;
314
340
  }
315
341
  continue;
@@ -352,15 +378,19 @@ export function emitTypes(tokens, sexpr = null, source = '') {
352
378
  hasDefault = true;
353
379
  }
354
380
 
355
- let isOptional = hasDefault || tok.data?.predicate;
381
+ let isOptional = hasDefault || tok.data?.optional;
356
382
  if (paramType) {
357
- params.push(`${paramName}${isOptional ? '?' : ''}: ${expandSuffixes(paramType)}`);
383
+ params.push(`${paramName}${isOptional ? '?' : ''}: ${tsType(paramType)}`);
358
384
  } else {
359
385
  params.push(paramName);
360
386
  }
361
387
  j++;
362
388
 
363
- // Skip past default value expression
389
+ // Skip past default value expression. Stops at the next top-level
390
+ // comma, the matching CALL_END, or the matching PARAM_END — the
391
+ // last is critical for arrow-function param lists wrapped in
392
+ // PARAM_START/PARAM_END (e.g. `(input, opts = {}) -> ...`),
393
+ // because PARAM_END is the only sentinel that closes the list.
364
394
  if (hasDefault) {
365
395
  j++; // skip =
366
396
  let dd = 0;
@@ -368,7 +398,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
368
398
  let dt = tokens[j];
369
399
  if (dt[0] === '(' || dt[0] === '[' || dt[0] === '{') dd++;
370
400
  if (dt[0] === ')' || dt[0] === ']' || dt[0] === '}') dd--;
371
- if (dd === 0 && (dt[1] === ',' || dt[0] === 'CALL_END')) break;
401
+ if (dd === 0 && (dt[1] === ',' || dt[0] === 'CALL_END' || dt[0] === 'PARAM_END')) break;
372
402
  j++;
373
403
  }
374
404
  }
@@ -381,10 +411,26 @@ export function emitTypes(tokens, sexpr = null, source = '') {
381
411
  return { params, fields, endIdx: j };
382
412
  };
383
413
 
414
+ let paramDepth = 0;
415
+ // bodyDepth counts how deep we are inside `def`/`->` bodies. At
416
+ // bodyDepth === 0 we're at module scope and dts.js may emit
417
+ // `declare function NAME(...)` for arrow assignments. Inside a
418
+ // function body, those assignments would be locals (e.g.
419
+ // `inst = (input, opts) -> ...` inside `def makeInstance`) and
420
+ // emitting a module-scope declare for them is wrong — TypeScript
421
+ // would treat the local and the (synthetic) global as separate
422
+ // bindings, causing the local to lose its type info.
423
+ let bodyDepth = 0;
384
424
  for (let i = 0; i < tokens.length; i++) {
385
425
  let t = tokens[i];
386
426
  let tag = t[0];
387
427
 
428
+ // Skip tokens inside parameter lists — annotations there describe
429
+ // the params of an enclosing function/method, not a top-level binding.
430
+ if (tag === 'PARAM_START') { paramDepth++; continue; }
431
+ if (tag === 'PARAM_END') { paramDepth--; continue; }
432
+ if (paramDepth > 0) continue;
433
+
388
434
  // Track export flag
389
435
  let exported = false;
390
436
  if (tag === 'EXPORT') {
@@ -394,18 +440,17 @@ export function emitTypes(tokens, sexpr = null, source = '') {
394
440
  t = tokens[i];
395
441
  tag = t[0];
396
442
 
397
- // Export default
443
+ // Export default — runtime statement, not a type declaration.
444
+ // The compiled JS body already carries `export default x`; emitting
445
+ // a duplicate here puts two `export default` declarations in front
446
+ // of the TypeScript language service and triggers TS2528. Skip the
447
+ // entire `export default ...` clause and let the body provide it.
398
448
  if (tag === 'DEFAULT') {
399
449
  i++;
400
- if (i >= tokens.length) break;
401
- t = tokens[i];
402
- tag = t[0];
403
-
404
- // export default IDENTIFIER (re-export)
405
- if (tag === 'IDENTIFIER') {
406
- lines.push(`${indent()}export default ${t[1]};`);
450
+ if (i < tokens.length) {
451
+ t = tokens[i];
452
+ tag = t[0];
407
453
  }
408
- // export default { ... } or other expressions — skip for now
409
454
  continue;
410
455
  }
411
456
  }
@@ -414,8 +459,21 @@ export function emitTypes(tokens, sexpr = null, source = '') {
414
459
  if (tag === 'IMPORT') {
415
460
  let importTokens = [];
416
461
  let j = i + 1;
417
- while (j < tokens.length && tokens[j][0] !== 'TERMINATOR') {
418
- importTokens.push(tokens[j]);
462
+ let depth = 0;
463
+ while (j < tokens.length) {
464
+ const tk = tokens[j];
465
+ const tg = tk[0];
466
+ if (tg === '{' || tg === '[' || tg === '(') depth++;
467
+ else if (tg === '}' || tg === ']' || tg === ')') depth--;
468
+ // Stop at top-level TERMINATOR; skip inner TERMINATORs and the
469
+ // synthetic INDENT/OUTDENT pair the lexer inserts inside multi-line
470
+ // braced import lists.
471
+ if (tg === 'TERMINATOR') {
472
+ if (depth <= 0) break;
473
+ j++; continue;
474
+ }
475
+ if (tg === 'INDENT' || tg === 'OUTDENT') { j++; continue; }
476
+ importTokens.push(tk);
419
477
  j++;
420
478
  }
421
479
  // Reconstruct: join with spaces, then clean up spacing
@@ -442,7 +500,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
442
500
  let nameToken = ot[1]; // DEF is [0], name is [1]
443
501
  let { params: paramList } = collectParams(ot, 2);
444
502
  let returnType = nameToken.data?.returnType;
445
- let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
503
+ let ret = returnType ? `: ${tsType(returnType)}` : '';
446
504
  let declare = inClass ? '' : (exp ? '' : 'declare ');
447
505
  let typeParams = data.typeParams || '';
448
506
  if (inClass) {
@@ -454,7 +512,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
454
512
  let ext = data.extends ? ` extends ${data.extends}` : '';
455
513
  emitBlock(`${exp}interface ${data.name}${params}${ext} `, data.typeText || '{}', '');
456
514
  } else {
457
- let typeText = expandSuffixes(data.typeText || '');
515
+ let typeText = tsType(data.typeText || '');
458
516
  emitBlock(`${exp}type ${data.name}${params} = `, typeText, ';');
459
517
  }
460
518
  continue;
@@ -514,9 +572,11 @@ export function emitTypes(tokens, sexpr = null, source = '') {
514
572
 
515
573
  // Check for extends
516
574
  let ext = '';
575
+ let isSubclass = false;
517
576
  let j = i + 2;
518
577
  if (tokens[j]?.[0] === 'EXTENDS') {
519
578
  ext = ` extends ${tokens[j + 1]?.[1] || ''}`;
579
+ isSubclass = true;
520
580
  j += 2;
521
581
  }
522
582
 
@@ -534,6 +594,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
534
594
  if (hasTypedMembers) {
535
595
  lines.push(`${indent()}${exp}declare class ${className}${ext} {`);
536
596
  inClass = true;
597
+ inSubclass = isSubclass;
537
598
  classFields.clear();
538
599
  indentLevel++;
539
600
  }
@@ -547,7 +608,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
547
608
  if (!nameToken) continue;
548
609
  let fnName = nameToken[1];
549
610
  let returnType = nameToken.data?.returnType;
550
- if (!returnType && nameToken.data?.await === true) returnType = 'void';
611
+ if (!returnType && nameToken.data?.bang === true) returnType = 'void';
551
612
  let typeParams = nameToken.data?.typeParams || '';
552
613
 
553
614
  let { params, endIdx } = collectParams(tokens, i + 2);
@@ -556,7 +617,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
556
617
  if (returnType || params.some(p => p.includes(':'))) {
557
618
  let exp = exported ? 'export ' : '';
558
619
  let declare = inClass ? '' : (exported ? '' : 'declare ');
559
- let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
620
+ let ret = returnType ? `: ${tsType(returnType)}` : '';
560
621
  let paramStr = params.join(', ');
561
622
  if (inClass) {
562
623
  lines.push(`${indent()}${fnName}${typeParams}(${paramStr})${ret};`);
@@ -593,7 +654,12 @@ export function emitTypes(tokens, sexpr = null, source = '') {
593
654
  let params = [];
594
655
  let fields = [];
595
656
  if (tokens[j]?.[0] === 'PARAM_START') {
596
- let result = collectParams(tokens, j);
657
+ // The compiler renames `@name` → `_name` on subclass
658
+ // constructors so `this.name = _name` runs after super();
659
+ // mirror that here so the shadow TS body's `_name`
660
+ // references resolve to a real param binding.
661
+ let isSubCtor = inSubclass && methodName === 'constructor';
662
+ let result = collectParams(tokens, j, isSubCtor);
597
663
  params = result.params;
598
664
  fields = result.fields;
599
665
  j = result.endIdx + 1;
@@ -612,7 +678,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
612
678
  }
613
679
 
614
680
  if (returnType || params.some(p => p.includes(':'))) {
615
- let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
681
+ let ret = returnType ? `: ${tsType(returnType)}` : '';
616
682
  let paramStr = params.join(', ');
617
683
  // Emit field declarations for constructor @param:: type shorthand
618
684
  if (methodName === 'constructor' && fields.length) {
@@ -637,19 +703,41 @@ export function emitTypes(tokens, sexpr = null, source = '') {
637
703
 
638
704
  // Track INDENT/OUTDENT for class body
639
705
  if (tag === 'INDENT') {
706
+ bodyDepth++;
640
707
  continue;
641
708
  }
642
709
  if (tag === 'OUTDENT') {
710
+ if (bodyDepth > 0) bodyDepth--;
643
711
  if (inClass) {
644
712
  indentLevel--;
645
713
  lines.push(`${indent()}}`);
646
714
  inClass = false;
715
+ inSubclass = false;
647
716
  }
648
717
  continue;
649
718
  }
650
719
 
651
720
  // Arrow function assignment: name = (params) -> body
652
- if (tag === 'IDENTIFIER' && !inClass && tokens[i + 1]?.[0] === '=' &&
721
+ //
722
+ // Emission shape depends on scope:
723
+ // bodyDepth === 0 → `declare function name(...): R;` at module
724
+ // scope. typecheck.js attaches this as an
725
+ // overload and copies typed params into the
726
+ // impl line.
727
+ // bodyDepth > 0 → `let name: (params) => R;` at module scope.
728
+ // typecheck.js's typed-local hoist pulls the
729
+ // `: (...) => R` into the body's `let name`,
730
+ // making the arrow contextually typed.
731
+ //
732
+ // The function-form is wrong inside bodies because it would create
733
+ // a phantom global that shadows / collides with the actual local.
734
+ //
735
+ // If the IDENTIFIER carries an explicit `name:: T = arrow` annotation,
736
+ // skip this path and let the typed-variable-assignment path below
737
+ // emit the typed form (otherwise we'd lose the user's annotation in
738
+ // favor of inferred `any` rest-param emission).
739
+ if (tag === 'IDENTIFIER' && !inClass && !t.data?.type &&
740
+ tokens[i + 1]?.[0] === '=' &&
653
741
  (tokens[i + 2]?.[0] === 'PARAM_START' || tokens[i + 2]?.[0] === '(')) {
654
742
  let fnName = t[1];
655
743
  let j = i + 2;
@@ -669,10 +757,22 @@ export function emitTypes(tokens, sexpr = null, source = '') {
669
757
 
670
758
  if (returnType || params.some(p => p.includes(':'))) {
671
759
  let exp = exported ? 'export ' : '';
672
- let declare = exported ? '' : 'declare ';
673
- let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
674
760
  let paramStr = params.join(', ');
675
- lines.push(`${indent()}${exp}${declare}function ${fnName}(${paramStr})${ret};`);
761
+ if (bodyDepth === 0) {
762
+ let declare = exported ? '' : 'declare ';
763
+ let ret = returnType ? `: ${tsType(returnType)}` : '';
764
+ lines.push(`${indent()}${exp}${declare}function ${fnName}(${paramStr})${ret};`);
765
+ } else {
766
+ // `any` rather than `unknown` for the inferred-return case.
767
+ // The annotation's job is to give the body's `let X` a typed
768
+ // function shape so call-site param checks engage; we
769
+ // intentionally don't constrain the return type when the
770
+ // user didn't provide one. `unknown` would force callers to
771
+ // narrow before using the result, creating new false
772
+ // positives at every call site.
773
+ let ret = returnType ? tsType(returnType) : 'any';
774
+ lines.push(`${indent()}let ${fnName}: (${paramStr}) => ${ret};`);
775
+ }
676
776
  continue;
677
777
  }
678
778
  }
@@ -680,7 +780,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
680
780
  // Variable assignments with type annotations
681
781
  if (tag === 'IDENTIFIER' && t.data?.type) {
682
782
  let varName = t[1];
683
- let type = expandSuffixes(t.data.type);
783
+ let type = tsType(t.data.type);
684
784
  let next = tokens[i + 1];
685
785
 
686
786
  if (next) {
@@ -714,7 +814,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
714
814
  if (arrowToken && (arrowToken[0] === '->' || arrowToken[0] === '=>') &&
715
815
  arrowToken.data?.returnType) {
716
816
  // Typed arrow function assignment
717
- let returnType = expandSuffixes(arrowToken.data.returnType);
817
+ let returnType = tsType(arrowToken.data.returnType);
718
818
  let { params } = collectParams(tokens, i + 2);
719
819
  let paramStr = params.join(', ');
720
820
  lines.push(`${indent()}${exp}${declare}function ${varName}(${paramStr}): ${returnType};`);
@@ -779,15 +879,18 @@ export function emitTypes(tokens, sexpr = null, source = '') {
779
879
  }
780
880
  if (usesSignal) {
781
881
  preamble.push(SIGNAL_INTERFACE);
782
- preamble.push(SIGNAL_FN);
882
+ if (!explicitlyBound.has('__state')) preamble.push(SIGNAL_FN);
783
883
  }
784
884
  if (usesComputed) {
785
885
  preamble.push(COMPUTED_INTERFACE);
786
- preamble.push(COMPUTED_FN);
886
+ if (!explicitlyBound.has('__computed')) preamble.push(COMPUTED_FN);
787
887
  }
788
- if (usesSignal || usesComputed) {
888
+ if ((usesSignal || usesComputed) && !explicitlyBound.has('__effect')) {
789
889
  preamble.push(EFFECT_FN);
790
890
  }
891
+ if ((usesSignal || usesComputed || usesBatch) && !explicitlyBound.has('__batch')) {
892
+ preamble.push(BATCH_FN);
893
+ }
791
894
  if (hasSchemaDecls) {
792
895
  preamble.push(...SCHEMA_INTRINSIC_DECLS);
793
896
  }
@@ -799,25 +902,16 @@ export function emitTypes(tokens, sexpr = null, source = '') {
799
902
  }
800
903
 
801
904
  // ============================================================================
802
- // Suffix expansion Rip type suffixes to TypeScript
905
+ // Convert a Rip type expression to its TypeScript form.
803
906
  // ============================================================================
907
+ //
908
+ // Today this only strips the `::` annotation sigil to `:`. Kept as a
909
+ // dedicated function so every call site routes through one place if the
910
+ // conversion ever needs to grow.
804
911
 
805
- function expandSuffixes(typeStr) {
912
+ function tsType(typeStr) {
806
913
  if (!typeStr) return typeStr;
807
-
808
- // Convert :: to : (annotation sigil to type separator)
809
- typeStr = typeStr.replace(/::/g, ':');
810
-
811
- // T?? → T | null | undefined
812
- typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?\?/g, '$1 | null | undefined');
813
-
814
- // T? → T | undefined (but not ?. or ?: which are different)
815
- typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?(?![.:])/g, '$1 | undefined');
816
-
817
- // T! → NonNullable<T>
818
- typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\!/g, 'NonNullable<$1>');
819
-
820
- return typeStr;
914
+ return typeStr.replace(/::/g, ':');
821
915
  }
822
916
 
823
917
  // ============================================================================
@@ -911,18 +1005,18 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
911
1005
  if (!Array.isArray(member)) continue;
912
1006
  let mHead = member[0]?.valueOf?.() ?? member[0];
913
1007
 
914
- let target, propName, isProp, type, hasDefault;
1008
+ let target, propName, isProp, type, optional;
915
1009
 
916
1010
  if (mHead === 'state' || mHead === 'readonly' || mHead === 'computed') {
917
1011
  target = member[1];
918
1012
  isProp = Array.isArray(target) && (target[0]?.valueOf?.() ?? target[0]) === '.' && (target[1]?.valueOf?.() ?? target[1]) === 'this';
919
1013
  propName = isProp ? (target[2]?.valueOf?.() ?? target[2]) : (target?.valueOf?.() ?? target);
920
1014
  type = isProp ? target[2]?.type : target?.type;
921
- hasDefault = true;
1015
+ optional = isProp ? !!target[2]?.optional : !!target?.optional;
922
1016
  if (!isProp) {
923
1017
  componentVars.add(propName);
924
1018
  let wrapper = (mHead === 'computed') ? 'Computed' : 'Signal';
925
- let typeStr = type ? expandSuffixes(type) : (inferLiteralType(member[2]) || 'any');
1019
+ let typeStr = type ? tsType(type) : (inferLiteralType(member[2]) || 'any');
926
1020
  bodyMembers.push(` ${propName}: ${wrapper}<${typeStr}>;`);
927
1021
  continue;
928
1022
  }
@@ -930,7 +1024,7 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
930
1024
  isProp = (member[1]?.valueOf?.() ?? member[1]) === 'this';
931
1025
  propName = isProp ? (member[2]?.valueOf?.() ?? member[2]) : null;
932
1026
  type = isProp ? member[2]?.type : null;
933
- hasDefault = false;
1027
+ optional = isProp ? !!member[2]?.optional : false;
934
1028
  if (!isProp && propName) componentVars.add(propName);
935
1029
  } else if (mHead === 'object') {
936
1030
  // Method definitions: (object (: methodName (-> (params...) (block ...))))
@@ -944,13 +1038,27 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
944
1038
  if (fHead !== '->' && fHead !== '=>') continue;
945
1039
  let params = funcDef[1];
946
1040
  if (!Array.isArray(params)) continue;
947
- let hasTypedParams = params.some(p => p?.type);
1041
+ // Unwrap `['default', name, value]` AST nodes (params with defaults
1042
+ // like `b = 1`) so we extract the underlying identifier rather than
1043
+ // stringifying the whole array via Array.toString — which produced
1044
+ // `default,b,1: any` in the emitted method signature.
1045
+ let unwrapDefault = (p) => {
1046
+ if (Array.isArray(p) && (p[0]?.valueOf?.() ?? p[0]) === 'default') {
1047
+ return { inner: p[1], hasDefault: true };
1048
+ }
1049
+ return { inner: p, hasDefault: false };
1050
+ };
1051
+ let hasTypedParams = params.some(p => unwrapDefault(p).inner?.type);
948
1052
  if (!hasTypedParams) continue;
949
1053
  let paramStrs = [];
950
1054
  for (let p of params) {
951
- let pName = p?.valueOf?.() ?? p;
952
- let pType = p?.type ? expandSuffixes(p.type) : 'any';
953
- paramStrs.push(`${pName}: ${pType}`);
1055
+ let { inner, hasDefault } = unwrapDefault(p);
1056
+ let pName = inner?.valueOf?.() ?? inner;
1057
+ let pType = inner?.type ? tsType(inner.type) : 'any';
1058
+ // Defaulted params are optional in the type signature so callers
1059
+ // may omit them.
1060
+ let opt = hasDefault ? '?' : '';
1061
+ paramStrs.push(`${pName}${opt}: ${pType}`);
954
1062
  }
955
1063
  bodyMembers.push(` ${methName}(${paramStrs.join(', ')}): void;`);
956
1064
  }
@@ -965,9 +1073,11 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
965
1073
 
966
1074
  if (!isProp || !propName) continue;
967
1075
 
968
- let typeStr = type ? expandSuffixes(type) : 'any';
969
- let opt = hasDefault ? '?' : '';
970
- if (!hasDefault) hasRequired = true;
1076
+ let typeStr = type ? tsType(type) : 'any';
1077
+ // `?` on the prop name is the sole optionality marker;
1078
+ // `:=` defaults do not imply optionality.
1079
+ let opt = optional ? '?' : '';
1080
+ if (!optional) hasRequired = true;
971
1081
  publicProps.push(` ${propName}${opt}: ${typeStr};`);
972
1082
  if (mHead === 'state') {
973
1083
  publicProps.push(` __bind_${propName}__?: Signal<${typeStr}>;`);
@@ -1014,7 +1124,7 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
1014
1124
  // Registration — install emitTypes into the compiler at module load.
1015
1125
  // ============================================================================
1016
1126
  // The compiler exposes setTypesEmitter() as a no-op-friendly hook. When
1017
- // nothing imports types-emit.js (browser bundle), the emitter stays null
1127
+ // nothing imports dts.js (browser bundle), the emitter stays null
1018
1128
  // and compile()s .d.ts output is silently skipped. CLI entry points and
1019
1129
  // typecheck.js import this module specifically to install the emitter.
1020
1130
 
@@ -140,7 +140,7 @@ names in the core generators:
140
140
  | `root` | Grammar start symbol | Root |
141
141
  | `body-list` | Left-recursive with TERMINATOR | Body, ComponentBody |
142
142
  | `comma-list` | Left-recursive with `,` | ArgList, ParamList |
143
- | `concat-list` | Left-recursive, no separator | Interpolations, Whens |
143
+ | `concat-list` | Left-recursive, no separator | Interpolations, Cases |
144
144
  | `left-rec-loop` | Self-referential with terminal continuation | IfBlock |
145
145
  | `expression` | Contains the operation nonterminal | Expression |
146
146
  | `operation` | Has binary operator rules | Operation |