rip-lang 3.16.0 → 3.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/dts.js CHANGED
@@ -36,11 +36,11 @@ export const INTRINSIC_TYPE_DECLS = [
36
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;",
37
37
  "type __RipEvents<K extends __RipTag> = { [E in keyof HTMLElementEventMap as `@${E}`]?: ((event: RipEvent<HTMLElementEventMap[E], __RipElementMap[K]>) => void) | null };",
38
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> | __RipClassValue[];',
39
+ 'type __RipClassValue = string | boolean | null | undefined | Record<string, boolean | null | undefined> | __RipClassValue[];',
40
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 };",
41
41
  ];
42
42
 
43
- 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;';
44
44
 
45
45
  export const ARIA_TYPE_DECLS = [
46
46
  'type __RipAriaNavHandlers = { next?: () => void; prev?: () => void; first?: () => void; last?: () => void; select?: () => void; dismiss?: () => void; tab?: () => void; char?: () => void; };',
@@ -67,6 +67,17 @@ export const SIGNAL_FN = 'declare function __state<T>(value: T | Signal<T>): Sig
67
67
  export const COMPUTED_INTERFACE = 'interface Computed<T> { readonly value: T; read(): T; lock(): Computed<T>; free(): Computed<T>; kill(): T; }';
68
68
  export const COMPUTED_FN = 'declare function __computed<T>(fn: () => T): Computed<T>;';
69
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
+ }
70
81
 
71
82
  // ============================================================================
72
83
  // emitTypes — generate .d.ts from annotated tokens + s-expression tree
@@ -82,7 +93,9 @@ export function emitTypes(tokens, sexpr = null, source = '') {
82
93
  let classFields = new Set(); // Track emitted field names to avoid duplicates
83
94
  let usesSignal = false;
84
95
  let usesComputed = false;
96
+ let usesBatch = false;
85
97
  let usesRipIntrinsicProps = false;
98
+ const explicitlyBound = ripDestructuredNames(source);
86
99
  const sourceLines = typeof source === 'string' ? source.split('\n') : [];
87
100
 
88
101
  // Pre-scan: detect reactive operators regardless of type annotations.
@@ -92,6 +105,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
92
105
  const tag = tokens[i][0];
93
106
  if (tag === 'REACTIVE_ASSIGN') usesSignal = true;
94
107
  else if (tag === 'COMPUTED_ASSIGN') usesComputed = true;
108
+ else if (tag === 'IDENTIFIER' && tokens[i][1] === '__batch') usesBatch = true;
95
109
  }
96
110
 
97
111
  // Format { prop; prop } into multi-line block. Only applies when the
@@ -195,7 +209,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
195
209
  let type = tokens[j].data?.type;
196
210
  if (type) hasAnyType = true;
197
211
  let hasDefault = tokens[j + 1]?.[0] === '=';
198
- 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 });
199
213
  j++;
200
214
  if (hasDefault) j = skipDefault(tokens, j);
201
215
  }
@@ -208,7 +222,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
208
222
  let type = tokens[j].data?.type;
209
223
  if (type) hasAnyType = true;
210
224
  let hasDefault = tokens[j + 1]?.[0] === '=';
211
- 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 });
212
226
  j++;
213
227
  if (hasDefault) j = skipDefault(tokens, j);
214
228
  continue;
@@ -259,7 +273,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
259
273
  let name = tokens[j][1];
260
274
  let type = tokens[j].data?.type;
261
275
  names.push(name);
262
- elemTypes.push(type ? expandSuffixes(type) : null);
276
+ elemTypes.push(type ? tsType(type) : null);
263
277
  if (type) hasAnyType = true;
264
278
  }
265
279
  j++;
@@ -308,8 +322,8 @@ export function emitTypes(tokens, sexpr = null, source = '') {
308
322
  let name = tokens[j][1];
309
323
  let type = tokens[j].data?.type;
310
324
  let paramName = subclassConstructor ? `_${name}` : name;
311
- params.push(type ? `${paramName}: ${expandSuffixes(type)}` : paramName);
312
- if (type) fields.push({ name, type: expandSuffixes(type) });
325
+ params.push(type ? `${paramName}: ${tsType(type)}` : paramName);
326
+ if (type) fields.push({ name, type: tsType(type) });
313
327
  j++;
314
328
  }
315
329
  continue;
@@ -321,7 +335,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
321
335
  if (tokens[j]?.[0] === 'IDENTIFIER') {
322
336
  let name = tokens[j][1];
323
337
  let type = tokens[j].data?.type;
324
- params.push(type ? `...${name}: ${expandSuffixes(type)}` : `...${name}: any[]`);
338
+ params.push(type ? `...${name}: ${tsType(type)}` : `...${name}: any[]`);
325
339
  j++;
326
340
  }
327
341
  continue;
@@ -364,9 +378,9 @@ export function emitTypes(tokens, sexpr = null, source = '') {
364
378
  hasDefault = true;
365
379
  }
366
380
 
367
- let isOptional = hasDefault || tok.data?.predicate;
381
+ let isOptional = hasDefault || tok.data?.optional;
368
382
  if (paramType) {
369
- params.push(`${paramName}${isOptional ? '?' : ''}: ${expandSuffixes(paramType)}`);
383
+ params.push(`${paramName}${isOptional ? '?' : ''}: ${tsType(paramType)}`);
370
384
  } else {
371
385
  params.push(paramName);
372
386
  }
@@ -486,7 +500,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
486
500
  let nameToken = ot[1]; // DEF is [0], name is [1]
487
501
  let { params: paramList } = collectParams(ot, 2);
488
502
  let returnType = nameToken.data?.returnType;
489
- let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
503
+ let ret = returnType ? `: ${tsType(returnType)}` : '';
490
504
  let declare = inClass ? '' : (exp ? '' : 'declare ');
491
505
  let typeParams = data.typeParams || '';
492
506
  if (inClass) {
@@ -498,7 +512,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
498
512
  let ext = data.extends ? ` extends ${data.extends}` : '';
499
513
  emitBlock(`${exp}interface ${data.name}${params}${ext} `, data.typeText || '{}', '');
500
514
  } else {
501
- let typeText = expandSuffixes(data.typeText || '');
515
+ let typeText = tsType(data.typeText || '');
502
516
  emitBlock(`${exp}type ${data.name}${params} = `, typeText, ';');
503
517
  }
504
518
  continue;
@@ -594,7 +608,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
594
608
  if (!nameToken) continue;
595
609
  let fnName = nameToken[1];
596
610
  let returnType = nameToken.data?.returnType;
597
- if (!returnType && nameToken.data?.await === true) returnType = 'void';
611
+ if (!returnType && nameToken.data?.bang === true) returnType = 'void';
598
612
  let typeParams = nameToken.data?.typeParams || '';
599
613
 
600
614
  let { params, endIdx } = collectParams(tokens, i + 2);
@@ -603,7 +617,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
603
617
  if (returnType || params.some(p => p.includes(':'))) {
604
618
  let exp = exported ? 'export ' : '';
605
619
  let declare = inClass ? '' : (exported ? '' : 'declare ');
606
- let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
620
+ let ret = returnType ? `: ${tsType(returnType)}` : '';
607
621
  let paramStr = params.join(', ');
608
622
  if (inClass) {
609
623
  lines.push(`${indent()}${fnName}${typeParams}(${paramStr})${ret};`);
@@ -664,7 +678,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
664
678
  }
665
679
 
666
680
  if (returnType || params.some(p => p.includes(':'))) {
667
- let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
681
+ let ret = returnType ? `: ${tsType(returnType)}` : '';
668
682
  let paramStr = params.join(', ');
669
683
  // Emit field declarations for constructor @param:: type shorthand
670
684
  if (methodName === 'constructor' && fields.length) {
@@ -717,7 +731,12 @@ export function emitTypes(tokens, sexpr = null, source = '') {
717
731
  //
718
732
  // The function-form is wrong inside bodies because it would create
719
733
  // a phantom global that shadows / collides with the actual local.
720
- if (tag === 'IDENTIFIER' && !inClass &&
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 &&
721
740
  tokens[i + 1]?.[0] === '=' &&
722
741
  (tokens[i + 2]?.[0] === 'PARAM_START' || tokens[i + 2]?.[0] === '(')) {
723
742
  let fnName = t[1];
@@ -741,7 +760,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
741
760
  let paramStr = params.join(', ');
742
761
  if (bodyDepth === 0) {
743
762
  let declare = exported ? '' : 'declare ';
744
- let ret = returnType ? `: ${expandSuffixes(returnType)}` : '';
763
+ let ret = returnType ? `: ${tsType(returnType)}` : '';
745
764
  lines.push(`${indent()}${exp}${declare}function ${fnName}(${paramStr})${ret};`);
746
765
  } else {
747
766
  // `any` rather than `unknown` for the inferred-return case.
@@ -751,7 +770,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
751
770
  // user didn't provide one. `unknown` would force callers to
752
771
  // narrow before using the result, creating new false
753
772
  // positives at every call site.
754
- let ret = returnType ? expandSuffixes(returnType) : 'any';
773
+ let ret = returnType ? tsType(returnType) : 'any';
755
774
  lines.push(`${indent()}let ${fnName}: (${paramStr}) => ${ret};`);
756
775
  }
757
776
  continue;
@@ -761,7 +780,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
761
780
  // Variable assignments with type annotations
762
781
  if (tag === 'IDENTIFIER' && t.data?.type) {
763
782
  let varName = t[1];
764
- let type = expandSuffixes(t.data.type);
783
+ let type = tsType(t.data.type);
765
784
  let next = tokens[i + 1];
766
785
 
767
786
  if (next) {
@@ -795,7 +814,7 @@ export function emitTypes(tokens, sexpr = null, source = '') {
795
814
  if (arrowToken && (arrowToken[0] === '->' || arrowToken[0] === '=>') &&
796
815
  arrowToken.data?.returnType) {
797
816
  // Typed arrow function assignment
798
- let returnType = expandSuffixes(arrowToken.data.returnType);
817
+ let returnType = tsType(arrowToken.data.returnType);
799
818
  let { params } = collectParams(tokens, i + 2);
800
819
  let paramStr = params.join(', ');
801
820
  lines.push(`${indent()}${exp}${declare}function ${varName}(${paramStr}): ${returnType};`);
@@ -860,15 +879,18 @@ export function emitTypes(tokens, sexpr = null, source = '') {
860
879
  }
861
880
  if (usesSignal) {
862
881
  preamble.push(SIGNAL_INTERFACE);
863
- preamble.push(SIGNAL_FN);
882
+ if (!explicitlyBound.has('__state')) preamble.push(SIGNAL_FN);
864
883
  }
865
884
  if (usesComputed) {
866
885
  preamble.push(COMPUTED_INTERFACE);
867
- preamble.push(COMPUTED_FN);
886
+ if (!explicitlyBound.has('__computed')) preamble.push(COMPUTED_FN);
868
887
  }
869
- if (usesSignal || usesComputed) {
888
+ if ((usesSignal || usesComputed) && !explicitlyBound.has('__effect')) {
870
889
  preamble.push(EFFECT_FN);
871
890
  }
891
+ if ((usesSignal || usesComputed || usesBatch) && !explicitlyBound.has('__batch')) {
892
+ preamble.push(BATCH_FN);
893
+ }
872
894
  if (hasSchemaDecls) {
873
895
  preamble.push(...SCHEMA_INTRINSIC_DECLS);
874
896
  }
@@ -880,25 +902,16 @@ export function emitTypes(tokens, sexpr = null, source = '') {
880
902
  }
881
903
 
882
904
  // ============================================================================
883
- // Suffix expansion Rip type suffixes to TypeScript
905
+ // Convert a Rip type expression to its TypeScript form.
884
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.
885
911
 
886
- function expandSuffixes(typeStr) {
912
+ function tsType(typeStr) {
887
913
  if (!typeStr) return typeStr;
888
-
889
- // Convert :: to : (annotation sigil to type separator)
890
- typeStr = typeStr.replace(/::/g, ':');
891
-
892
- // T?? → T | null | undefined
893
- typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?\?/g, '$1 | null | undefined');
894
-
895
- // T? → T | undefined (but not ?. or ?: which are different)
896
- typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\?(?![.:])/g, '$1 | undefined');
897
-
898
- // T! → NonNullable<T>
899
- typeStr = typeStr.replace(/(\w+(?:<[^>]+>)?)\!/g, 'NonNullable<$1>');
900
-
901
- return typeStr;
914
+ return typeStr.replace(/::/g, ':');
902
915
  }
903
916
 
904
917
  // ============================================================================
@@ -992,18 +1005,18 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
992
1005
  if (!Array.isArray(member)) continue;
993
1006
  let mHead = member[0]?.valueOf?.() ?? member[0];
994
1007
 
995
- let target, propName, isProp, type, hasDefault;
1008
+ let target, propName, isProp, type, optional;
996
1009
 
997
1010
  if (mHead === 'state' || mHead === 'readonly' || mHead === 'computed') {
998
1011
  target = member[1];
999
1012
  isProp = Array.isArray(target) && (target[0]?.valueOf?.() ?? target[0]) === '.' && (target[1]?.valueOf?.() ?? target[1]) === 'this';
1000
1013
  propName = isProp ? (target[2]?.valueOf?.() ?? target[2]) : (target?.valueOf?.() ?? target);
1001
1014
  type = isProp ? target[2]?.type : target?.type;
1002
- hasDefault = true;
1015
+ optional = isProp ? !!target[2]?.optional : !!target?.optional;
1003
1016
  if (!isProp) {
1004
1017
  componentVars.add(propName);
1005
1018
  let wrapper = (mHead === 'computed') ? 'Computed' : 'Signal';
1006
- let typeStr = type ? expandSuffixes(type) : (inferLiteralType(member[2]) || 'any');
1019
+ let typeStr = type ? tsType(type) : (inferLiteralType(member[2]) || 'any');
1007
1020
  bodyMembers.push(` ${propName}: ${wrapper}<${typeStr}>;`);
1008
1021
  continue;
1009
1022
  }
@@ -1011,7 +1024,7 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
1011
1024
  isProp = (member[1]?.valueOf?.() ?? member[1]) === 'this';
1012
1025
  propName = isProp ? (member[2]?.valueOf?.() ?? member[2]) : null;
1013
1026
  type = isProp ? member[2]?.type : null;
1014
- hasDefault = false;
1027
+ optional = isProp ? !!member[2]?.optional : false;
1015
1028
  if (!isProp && propName) componentVars.add(propName);
1016
1029
  } else if (mHead === 'object') {
1017
1030
  // Method definitions: (object (: methodName (-> (params...) (block ...))))
@@ -1041,7 +1054,7 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
1041
1054
  for (let p of params) {
1042
1055
  let { inner, hasDefault } = unwrapDefault(p);
1043
1056
  let pName = inner?.valueOf?.() ?? inner;
1044
- let pType = inner?.type ? expandSuffixes(inner.type) : 'any';
1057
+ let pType = inner?.type ? tsType(inner.type) : 'any';
1045
1058
  // Defaulted params are optional in the type signature so callers
1046
1059
  // may omit them.
1047
1060
  let opt = hasDefault ? '?' : '';
@@ -1060,9 +1073,11 @@ function emitComponentTypes(sexpr, lines, indent, indentLevel, componentVars, so
1060
1073
 
1061
1074
  if (!isProp || !propName) continue;
1062
1075
 
1063
- let typeStr = type ? expandSuffixes(type) : 'any';
1064
- let opt = hasDefault ? '?' : '';
1065
- 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;
1066
1081
  publicProps.push(` ${propName}${opt}: ${typeStr};`);
1067
1082
  if (mHead === 'state') {
1068
1083
  publicProps.push(` __bind_${propName}__?: Signal<${typeStr}>;`);
package/src/lexer.js CHANGED
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // Design principles:
9
9
  // - Every token carries .pre (whitespace count before it)
10
- // - Every token carries .data (metadata: await, predicate, quote, etc.)
10
+ // - Every token carries .data (metadata: bang, optional, quote, etc.)
11
11
  // - Every token carries .loc (location: row, col, len)
12
12
  // - Indentation is derived from .pre, not tracked during lexing
13
13
  // - Token categories use Sets for O(1) membership tests
@@ -23,8 +23,8 @@
23
23
  // token.newLine — true if preceded by a newline
24
24
  //
25
25
  // Identifier suffixes:
26
- // ! — dammit operator: fetch!() → await fetch()
27
- // ? — predicate: empty? → isEmpty (returns boolean convention)
26
+ // ! — bang: fetch!() → await fetch() (call site) | foo! = -> → void (definition)
27
+ // ? — optional: empty? → (empty != null) (existence check / optional marker)
28
28
  //
29
29
  // The 9 tokenizer methods (in priority order):
30
30
  // 1. identifier — variables, keywords, properties, ! and ? suffixes
@@ -246,7 +246,7 @@ let UNARY_MATH = new Set(['!', '~']);
246
246
  // Regex Patterns
247
247
  // ==========================================================================
248
248
 
249
- // Identifier: word chars + optional trailing ! (await) or ? (predicate)
249
+ // Identifier: word chars + optional trailing ! (bang) or ? (optional)
250
250
  // The ? suffix is only captured when NOT followed by . ? ! [ ( to avoid
251
251
  // conflict with ?. (optional chaining), ?? (nullish), ?! (presence), ?.( and ?.[
252
252
  let IDENTIFIER_RE = /^(?!\d)((?:(?!\s)[$\w\x7f-\uffff])+(?:!|[?](?![.?![(]))?)([^\n\S]*:(?![=:]))?/;
@@ -519,8 +519,10 @@ export class Lexer {
519
519
  // Handles: variables, keywords, properties, aliases
520
520
  //
521
521
  // Suffix operators on identifiers:
522
- // ! → dammit operator (await): fetch!() await fetch()
523
- // ? → predicate (boolean): empty? isEmpty
522
+ // ! → bang: a neutral "trailing !" flag. Resolved by context downstream —
523
+ // dammit/await at a call site (fetch!() await fetch()), or the void
524
+ // marker at a function definition (foo! = -> / def foo! → no return).
525
+ // ? → optional (existence): empty? → (empty != null)
524
526
  //
525
527
  // The ? suffix is captured by IDENTIFIER_RE only when NOT followed by
526
528
  // . ? [ ( — so x?.y (optional chaining) and x?? (nullish coalescing)
@@ -655,16 +657,20 @@ export class Lexer {
655
657
  }
656
658
  }
657
659
 
658
- // --- Dammit operator: trailing ! → await ---
660
+ // --- Bang: trailing ! → context-resolved (await at call site / void at def) ---
659
661
  if (id.length > 1 && id.endsWith('!')) {
660
- data.await = true;
662
+ data.bang = true;
661
663
  id = id.slice(0, -1);
662
664
  }
663
665
 
664
- // --- Predicate operator: trailing ? → boolean convention ---
665
- // empty? isEmpty, active?isActive, valid? → isValid
666
+ // --- Optional marker: trailing ? ---
667
+ // Identifier-position: existence check (`empty?``(empty != null)`).
668
+ // Property-name position: optional prop / type field (`@label?:: T`,
669
+ // `{ x?:: T }`, `def f(x?:: T)`). The flag is consumed by the type/
670
+ // component/schema emitters; the runtime semantics for `name?` as an
671
+ // existence check are unchanged.
666
672
  if (id.length > 1 && id.endsWith('?')) {
667
- data.predicate = true;
673
+ data.optional = true;
668
674
  id = id.slice(0, -1);
669
675
  }
670
676
 
@@ -1519,9 +1525,42 @@ export class Lexer {
1519
1525
 
1520
1526
  // Walk back to tag parameters for arrow functions
1521
1527
  tagParameters() {
1522
- if (this.prevTag() !== ')') return this.tagDoIife();
1528
+ let closeIdx = this.tokens.length - 1;
1529
+ if (this.tokens[closeIdx]?.[0] !== ')') {
1530
+ // Maybe a return-type annotation sits between `)` and the arrow:
1531
+ // `(x:: T):: R ->`. Scan backward over the type expression (balanced
1532
+ // brackets) looking for the trailing `TYPE_ANNOTATION` whose previous
1533
+ // token is `)`. If found, treat that `)` as the param-list close.
1534
+ let n = this.tokens.length;
1535
+ let depth = 0;
1536
+ let j = n - 1;
1537
+ let found = -1;
1538
+ while (j >= 0) {
1539
+ let tk = this.tokens[j];
1540
+ let tg = tk[0];
1541
+ if (tg === ')' || tg === ']' || tg === '}' || tg === 'CALL_END' ||
1542
+ tg === 'PARAM_END' || tg === 'INDEX_END' ||
1543
+ (tg === 'COMPARE' && tk[1] === '>')) {
1544
+ depth++;
1545
+ } else if (tg === '(' || tg === '[' || tg === '{' || tg === 'CALL_START' ||
1546
+ tg === 'PARAM_START' || tg === 'INDEX_START' ||
1547
+ (tg === 'COMPARE' && tk[1] === '<')) {
1548
+ depth--;
1549
+ } else if (depth === 0) {
1550
+ if (tg === 'TYPE_ANNOTATION') {
1551
+ if (j > 0 && this.tokens[j - 1][0] === ')') found = j - 1;
1552
+ break;
1553
+ }
1554
+ if (tg === 'TERMINATOR' || tg === 'INDENT' || tg === 'OUTDENT' ||
1555
+ tg === '=' || tg === '->' || tg === '=>') break;
1556
+ }
1557
+ j--;
1558
+ }
1559
+ if (found < 0) return this.tagDoIife();
1560
+ closeIdx = found;
1561
+ }
1523
1562
 
1524
- let i = this.tokens.length - 1;
1563
+ let i = closeIdx;
1525
1564
  let stack = [];
1526
1565
  this.tokens[i][0] = 'PARAM_END';
1527
1566
 
@@ -1536,7 +1575,7 @@ export class Lexer {
1536
1575
  tok[0] = 'PARAM_START';
1537
1576
  return this.tagDoIife(i - 1);
1538
1577
  } else {
1539
- this.tokens[this.tokens.length - 1][0] = 'CALL_END';
1578
+ this.tokens[closeIdx][0] = 'CALL_END';
1540
1579
  return;
1541
1580
  }
1542
1581
  }
@@ -1562,10 +1601,14 @@ export class Lexer {
1562
1601
  this.closeMergeAssignments();
1563
1602
  this.closeOpenCalls();
1564
1603
  this.closeOpenIndexes();
1604
+ // rewriteTypes must run BEFORE normalizeLines: otherwise a type-arrow
1605
+ // `=>` inside `(...) => T` gets treated as a single-liner function and
1606
+ // wrapped in spurious INDENT/OUTDENT, which then derails the type
1607
+ // collector (it slurps the whole assignment body as part of the type).
1608
+ this.rewriteTypes();
1565
1609
  this.normalizeLines();
1566
1610
  this.rewriteRender?.();
1567
1611
  this.rewriteSchema?.();
1568
- this.rewriteTypes();
1569
1612
  this.tagPostfixConditionals();
1570
1613
  this.rewriteTaggedTemplates();
1571
1614
  this.addImplicitBracesAndParens();
@@ -541,7 +541,7 @@ function parseFieldedLine(kind, line, entries, ctx) {
541
541
  dname === 'one' || dname === 'many' || dname === 'mixin') {
542
542
  let t0 = argTokens[0];
543
543
  if (t0 && (t0[0] === 'IDENTIFIER' || t0[0] === 'PROPERTY')) {
544
- let optional = t0.data?.predicate === true;
544
+ let optional = t0.data?.optional === true;
545
545
  if (!optional && argTokens[1]?.[0] === '?') optional = true;
546
546
  args = [{ target: t0[1], optional }];
547
547
  }
@@ -1468,9 +1468,9 @@ function compileDirectiveArgsLiteral(name, tokens) {
1468
1468
  }
1469
1469
  let target = t0[1];
1470
1470
  // `@belongs_to User?` tokenizes as IDENTIFIER "User" with
1471
- // data.predicate=true. A trailing `?` in a later token position is
1471
+ // data.optional=true. A trailing `?` in a later token position is
1472
1472
  // also accepted for robustness.
1473
- let optional = t0.data?.predicate === true;
1473
+ let optional = t0.data?.optional === true;
1474
1474
  let pos = 1;
1475
1475
  if (!optional && tokens[pos]?.[0] === '?') { optional = true; pos++; }
1476
1476
  let parts = [`target: ${JSON.stringify(target)}`];
@@ -1673,8 +1673,8 @@ function parseBodyTokens(bodyTokens) {
1673
1673
  function collectModifiers(identToken) {
1674
1674
  let mods = [];
1675
1675
  let d = identToken.data;
1676
- if (d?.await === true) mods.push('!');
1677
- if (d?.predicate === true) mods.push('?');
1676
+ if (d?.bang === true) mods.push('!');
1677
+ if (d?.optional === true) mods.push('?');
1678
1678
  return mods;
1679
1679
  }
1680
1680