rip-lang 3.16.3 → 3.17.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/types.js CHANGED
@@ -12,6 +12,8 @@
12
12
  // compiler.js (CodeEmitter.prototype.emitEnum) — that's real codegen,
13
13
  // not type machinery.
14
14
 
15
+ import { RipError } from './error.js';
16
+
15
17
  // ============================================================================
16
18
  // installTypeSupport — adds rewriteTypes() to Lexer.prototype
17
19
  // ============================================================================
@@ -35,6 +37,14 @@ export function installTypeSupport(Lexer) {
35
37
  proto.rewriteTypes = function() {
36
38
  let tokens = this.tokens;
37
39
  let typeRefNames = this.typeRefNames = new Set();
40
+
41
+ // Rip 3.17 — single-colon type annotations. A `:` in a type-annotation
42
+ // slot (function params, return types, statement-level typed declarations)
43
+ // is reclassified to TYPE_ANNOTATION so it flows through the same machinery
44
+ // as the explicit `::`. Object properties, ternary branches, and nested
45
+ // colons are deliberately left untouched. Runs before the main type scan.
46
+ reclassifyColonTypes(tokens);
47
+
38
48
  let gen = (tag, val, origin) => {
39
49
  let t = [tag, val];
40
50
  t.pre = 0;
@@ -88,7 +98,35 @@ export function installTypeSupport(Lexer) {
88
98
  let prevToken = tokens[i - 1];
89
99
  if (!prevToken) return 1;
90
100
 
91
- let typeTokens = collectTypeExpression(tokens, i + 1);
101
+ // An arrow return type — `(params):: T -> body` / `(params):: T => body`
102
+ // — is uniquely flagged by a preceding `PARAM_END` (arrow param close).
103
+ // There the trailing `=>` is the arrow OPERATOR, not a TS function-type
104
+ // arrow, so the collector must stop at a depth-0 `=>` (it already stops
105
+ // at `->`). Without the flag it would eat the `=>` and the body as a
106
+ // function type, and the parser would never see the arrow.
107
+ let isArrowReturn = prevToken[0] === 'PARAM_END';
108
+ let typeTokens = collectTypeExpression(tokens, i + 1, { stopAtFatArrow: isArrowReturn });
109
+
110
+ // Foot-gun guard: a function-TYPE return must be parenthesized as a
111
+ // whole — `(x):: ((a: T) => R) => body`, not `(x):: (a: T) => R => body`.
112
+ // Unwrapped, the collector stops at the inner `=>` and the return type
113
+ // comes back as a bare parameter list `(a: T)` (a single `(…)` group
114
+ // with a top-level `:` , or empty `()`), which is never a valid type.
115
+ // Catch that exact shape and point at the fix rather than silently
116
+ // re-associating the body.
117
+ if (isArrowReturn && looksLikeBareFunctionType(typeTokens)) {
118
+ let loc = tokens[i + 1]?.loc;
119
+ throw new RipError('A function-type return on an arrow must be parenthesized', {
120
+ code: 'E_SYNTAX',
121
+ phase: 'lexer',
122
+ line: loc?.r ?? null,
123
+ column: loc?.c ?? null,
124
+ length: loc?.n ?? 1,
125
+ suggestion: 'wrap the whole function type in parens — ' +
126
+ '`(x):: ((a: T) => R) => body`, not `(x):: (a: T) => R => body`',
127
+ });
128
+ }
129
+
92
130
  let typeStr = buildTypeString(typeTokens);
93
131
 
94
132
  // Find the token that survives into the s-expression
@@ -312,8 +350,425 @@ export function installTypeSupport(Lexer) {
312
350
  // Type expression collection helpers
313
351
  // ============================================================================
314
352
 
353
+ // Does a collected arrow-return type look like a bare (un-parenthesized)
354
+ // function type — i.e. a single balanced `(…)` group spanning the whole type
355
+ // whose top level is a parameter list (an empty `()`, or a depth-1 `:`)? Such
356
+ // a "type" is only valid as the params of a function type, so seeing it in
357
+ // return position means the user wrote `(x):: (a: T) => R => body` and forgot
358
+ // to wrap the function type: `(x):: ((a: T) => R) => body`.
359
+ function looksLikeBareFunctionType(typeTokens) {
360
+ const isOpen = t => t === '(' || t === 'PARAM_START' || t === 'CALL_START';
361
+ const isClose = t => t === ')' || t === 'PARAM_END' || t === 'CALL_END';
362
+ if (typeTokens.length < 2 || !isOpen(typeTokens[0][0])) return false;
363
+
364
+ // The opening paren must match the LAST token: one group spanning the type.
365
+ let depth = 0;
366
+ for (let k = 0; k < typeTokens.length; k++) {
367
+ const tag = typeTokens[k][0];
368
+ if (isOpen(tag)) depth++;
369
+ else if (isClose(tag)) {
370
+ if (--depth === 0 && k !== typeTokens.length - 1) return false;
371
+ }
372
+ }
373
+ if (depth !== 0) return false;
374
+
375
+ if (typeTokens.length === 2) return true; // empty `()`
376
+
377
+ // A `:` directly inside the outer parens (depth 1) ⇒ parameter-list shape.
378
+ // Brackets/braces nested inside don't count (e.g. `(string | number)` and the
379
+ // object literal `({ a: number })` have no depth-1 `:`). One more exclusion:
380
+ // a parenthesized CONDITIONAL type `(T extends U ? A : B)` has a depth-1 `:`,
381
+ // but it's the conditional's else-branch, always paired with a preceding `?`
382
+ // (TERNARY) at the same depth — a parameter `:` never is. Track that so a
383
+ // conditional return type isn't mistaken for a bare parameter list.
384
+ let d = 0, pendingTernary = false;
385
+ for (const t of typeTokens) {
386
+ const tag = t[0];
387
+ if (isOpen(tag) || tag === '[' || tag === '{' || tag === 'INDEX_START') d++;
388
+ else if (isClose(tag) || tag === ']' || tag === '}' || tag === 'INDEX_END') d--;
389
+ else if (d === 1 && tag === 'TERNARY') pendingTernary = true;
390
+ else if (d === 1 && tag === ':') {
391
+ if (pendingTernary) pendingTernary = false; // conditional else-branch
392
+ else return true; // parameter-list colon
393
+ }
394
+ }
395
+ return false;
396
+ }
397
+
398
+ // Rip 3.17 — does the token slice [a, b) form a complete, well-formed *type*
399
+ // expression (vs a value expression)? Used to decide whether a single `:` in a
400
+ // bare decl (`r: R`) or a function-type-valued decl (`get: (p) => void = …`) is
401
+ // a type annotation. A whitelist of type tokens + bracket/generic balance + an
402
+ // adjacency rule (two atoms with no separator is not a type — kills the value
403
+ // comparison `x < y > z`). Anything outside the whitelist (a CALL_START, `->`,
404
+ // `new`/UNARY, arithmetic, `&&`, `==`, …) marks it as a value, so the `:` is
405
+ // left alone and any side effects survive.
406
+ function isCompleteTypeExpr(tokens, a, b) {
407
+ if (b <= a) return false;
408
+ const SEP = new Set(['|', '&', ',', ':', '?', '.', '...', '=>']);
409
+ let par = 0, brk = 0, brc = 0, gen = 0, atomEnd = false;
410
+ for (let j = a; j < b; j++) {
411
+ const t = tokens[j][0], v = tokens[j][1];
412
+ if (t === '(' || t === 'PARAM_START') { par++; atomEnd = false; continue; }
413
+ if (t === ')' || t === 'PARAM_END') { if (--par < 0) return false; atomEnd = true; continue; }
414
+ if (t === '[' || t === 'INDEX_START') { brk++; atomEnd = false; continue; }
415
+ if (t === ']' || t === 'INDEX_END') { if (--brk < 0) return false; atomEnd = true; continue; }
416
+ if (t === '{') { brc++; atomEnd = false; continue; }
417
+ if (t === '}') { if (--brc < 0) return false; atomEnd = true; continue; }
418
+ if (t === 'COMPARE') {
419
+ if (v === '<') { gen++; atomEnd = false; continue; }
420
+ if (v === '>') { if (gen <= 0) return false; gen--; atomEnd = true; continue; }
421
+ return false; // ==, !=, <=, >= → not a type
422
+ }
423
+ if (t === 'SHIFT') {
424
+ if (v === '>>') { if (gen < 2) return false; gen -= 2; atomEnd = true; continue; }
425
+ return false;
426
+ }
427
+ if (t === '=') { if (gen > 0) { atomEnd = false; continue; } return false; } // generic default only
428
+ if (SEP.has(t)) { atomEnd = false; continue; }
429
+ if (t === 'IDENTIFIER' || t === 'PROPERTY' || t === 'NUMBER' ||
430
+ t === 'STRING' || t === 'NULL' || t === 'UNDEFINED' || t === 'BOOL') {
431
+ if (atomEnd) return false; // two atoms, no separator → not a type
432
+ atomEnd = true; continue;
433
+ }
434
+ return false; // any non-type token → value
435
+ }
436
+ return par === 0 && brk === 0 && brc === 0 && gen === 0 && atomEnd;
437
+ }
438
+
439
+ // Rip 3.17 — positive evidence that a bare `name: T` is a typed forward
440
+ // declaration: the same identifier is assigned (`name = …`, `:=`, etc.)
441
+ // somewhere later in the same block (any nesting depth before the block's
442
+ // OUTDENT). Distinguishes `r: R` (then `r = …`) from a discarded object
443
+ // property. Starts scanning at `start` (just past the decl line's TERMINATOR).
444
+ function assignedLaterInBlock(tokens, start, name) {
445
+ const ASSIGN = new Set(['=', 'REACTIVE_ASSIGN', 'COMPUTED_ASSIGN', 'READONLY_ASSIGN']);
446
+ // Descend into control-flow sub-blocks (try/if/for — the medlabs `r = …`
447
+ // sits inside a `try`), but SKIP nested function bodies: an assignment inside
448
+ // a closure (`init -> … name = …`) is a different scope and must not count
449
+ // as evidence. A closure body is an INDENT immediately preceded by `->`/`=>`.
450
+ let indent = 0, bracket = 0, closureDepth = 0;
451
+ const closureAt = [];
452
+ for (let j = start; j < tokens.length; j++) {
453
+ const g = tokens[j][0];
454
+ if (g === 'INDENT') {
455
+ indent++;
456
+ const p = tokens[j - 1]?.[0];
457
+ if (p === '->' || p === '=>') { closureAt.push(indent); closureDepth++; }
458
+ continue;
459
+ }
460
+ if (g === 'OUTDENT') {
461
+ if (indent === 0) return false; // left the starting block
462
+ if (closureAt[closureAt.length - 1] === indent) { closureAt.pop(); closureDepth--; }
463
+ indent--; continue;
464
+ }
465
+ if (g === '(' || g === 'CALL_START' || g === 'PARAM_START' ||
466
+ g === '[' || g === 'INDEX_START' || g === '{') { bracket++; continue; }
467
+ if (g === ')' || g === 'CALL_END' || g === 'PARAM_END' ||
468
+ g === ']' || g === 'INDEX_END' || g === '}') { bracket--; continue; }
469
+ if (closureDepth === 0 && bracket === 0 && (g === 'IDENTIFIER' || g === 'PROPERTY') &&
470
+ tokens[j][1] === name && ASSIGN.has(tokens[j + 1]?.[0])) return true;
471
+ }
472
+ return false;
473
+ }
474
+
475
+ // Rip 3.17 — reclassify single-colon `:` into TYPE_ANNOTATION in the contexts
476
+ // where it means a type, so `:` and `::` share one code path. Scoped tightly:
477
+ //
478
+ // - function parameters: `(x: T)`, `(x: T = d)` (arrow PARAM_START + def)
479
+ // - return types: `(…): T ->`, `def f(…): T`
480
+ // - statement decls: `x: T = v` (binding name at statement start)
481
+ //
482
+ // Everything else keeps `:` as-is: object properties (`{x: 1}`, `foo a: 1`),
483
+ // implicit-object call args (`foo(a: 1)`), ternary (`a ? b : c`), and any colon
484
+ // nested inside `{}`/`[]`/`<>` (object-type interiors, generic args). The
485
+ // preceding object-key `PROPERTY` token is retagged back to `IDENTIFIER` so the
486
+ // downstream `::` handler treats it as a binder, not a property.
487
+ //
488
+ // Frame state is per parameter segment: a `:` is a param type only before any
489
+ // top-level `=` (so a ternary in a default `(x = a ? b : c)` is untouched) and
490
+ // only the first colon in the segment.
491
+ function reclassifyColonTypes(tokens) {
492
+ const isOpen = t => t === '(' || t === 'CALL_START' || t === 'PARAM_START' ||
493
+ t === '{' || t === '[' || t === 'INDEX_START';
494
+ const isClose = t => t === ')' || t === 'CALL_END' || t === 'PARAM_END' ||
495
+ t === '}' || t === ']' || t === 'INDEX_END';
496
+
497
+ // Is this CALL_START the param list of a `def`? Matches `DEF IDENT (` and
498
+ // `DEF IDENT <generics> (`. Deliberately narrow so an ordinary call
499
+ // `foo(a: 1)` (no preceding DEF) is never mistaken for a def param list.
500
+ const isDefParamStart = (i) => {
501
+ let j = i - 1;
502
+ // Skip a balanced generic list `<…>` between the def name and `(`.
503
+ const g0 = tokens[j]?.[0], v0 = tokens[j]?.[1];
504
+ if ((g0 === 'COMPARE' && v0 === '>') || (g0 === 'SHIFT' && v0 === '>>')) {
505
+ let depth = g0 === 'SHIFT' ? 2 : 1;
506
+ j--;
507
+ while (j >= 0 && depth > 0) {
508
+ const g = tokens[j][0], v = tokens[j][1];
509
+ if (g === 'COMPARE' && v === '>') depth++;
510
+ else if (g === 'SHIFT' && v === '>>') depth += 2;
511
+ else if (g === 'COMPARE' && v === '<') depth--;
512
+ else if (g === 'SHIFT' && v === '<<') depth -= 2;
513
+ j--;
514
+ }
515
+ }
516
+ if (tokens[j]?.[0] !== 'IDENTIFIER' && tokens[j]?.[0] !== 'PROPERTY') return false;
517
+ return tokens[j - 1]?.[0] === 'DEF';
518
+ };
519
+
520
+ // Statement-level typed declaration `name: T = v` (and `:=`, `~=`, `=!`,
521
+ // `~>`, `<~`). True only when an assignment/binding operator appears at the
522
+ // top level BEFORE any function arrow — so a method/value like
523
+ // `name: (x) -> …` or `name: (x) => y = x` (arrow first) stays a binding,
524
+ // and a bare `name: T` (no binding op) stays an object property.
525
+ const STMT_START = new Set(['TERMINATOR', 'INDENT', 'OUTDENT', 'EXPORT']);
526
+ // Assignment/binding operators that mark `name: T <op> …` as a typed
527
+ // declaration. EFFECT (`~>`) and GATE (`<~`) are deliberately excluded: a
528
+ // bare `name: ~> …` is a schema computed getter (no type), so treating `~>`
529
+ // as a binding op would misread schema bodies as typed declarations.
530
+ const BINDING_OPS = new Set(['=', 'REACTIVE_ASSIGN', 'COMPUTED_ASSIGN', 'READONLY_ASSIGN']);
531
+ const atStatementStart = (t) => !t || STMT_START.has(t[0]);
532
+ const declBindsBeforeArrow = (start) => {
533
+ let depth = 0;
534
+ for (let j = start; j < tokens.length; j++) {
535
+ const g = tokens[j][0];
536
+ if (isOpen(g)) depth++;
537
+ else if (isClose(g)) depth--;
538
+ else if (depth === 0) {
539
+ if (g === '->' || g === '=>') return false;
540
+ if (BINDING_OPS.has(g)) return true;
541
+ // Typed effect/gate: `name: T ~> …` — an EFFECT/GATE that is NOT the
542
+ // first token after `:` follows a type (a bare `name: ~> …` schema
543
+ // getter has it first, so j === start, and stays excluded).
544
+ if ((g === 'EFFECT' || g === 'GATE') && j > start) return true;
545
+ if (g === 'TERMINATOR' || g === 'INDENT' || g === 'OUTDENT') return false;
546
+ }
547
+ }
548
+ return false;
549
+ };
550
+
551
+ // Does the value after a class-member `:` look like a function (a method),
552
+ // i.e. does a `->`/`=>` arrow appear at depth 0 before the member ends? If so
553
+ // it's a method binding, not a typed field — leave it alone.
554
+ const valueIsMethod = (start) => {
555
+ let depth = 0;
556
+ for (let j = start; j < tokens.length; j++) {
557
+ const g = tokens[j][0];
558
+ if (isOpen(g)) depth++;
559
+ else if (isClose(g)) depth--;
560
+ else if (depth === 0) {
561
+ if (g === '->' || g === '=>') return true;
562
+ if (g === 'TERMINATOR' || g === 'INDENT' || g === 'OUTDENT') return false;
563
+ }
564
+ }
565
+ return false;
566
+ };
567
+
568
+ // Track class/component bodies via INDENT/OUTDENT so a bare `name: T` member
569
+ // (or `@name: T` component prop) is read as a typed field. `pendingBody` is
570
+ // armed by `class`/`component` and consumed by the next INDENT (the body);
571
+ // methods inside it open their own (non-body) INDENT, so a member is in a
572
+ // typed body only when the innermost indent frame is that body.
573
+ let pendingBody = false;
574
+ const indentStack = [];
575
+ const inTypedBody = () => indentStack.length > 0 && indentStack[indentStack.length - 1];
576
+
577
+ // The binding name for a statement/field annotation is `tokens[i-1]` (a
578
+ // PROPERTY/IDENTIFIER), optionally preceded by `@` (component prop / promoted
579
+ // field). Returns true when that name sits at a statement boundary.
580
+ const nameAtStatementStart = (i) => {
581
+ const p = tokens[i - 1]?.[0];
582
+ if (p !== 'PROPERTY' && p !== 'IDENTIFIER') return false;
583
+ if (tokens[i - 2]?.[0] === '@') return atStatementStart(tokens[i - 3]);
584
+ return atStatementStart(tokens[i - 2]);
585
+ };
586
+
587
+ // A type expression (opened by any `::` / TYPE_ANNOTATION, or by a `:` we
588
+ // reclassify) must never have its interior touched: a function type like
589
+ // `{ cb: (r: R) => void }` carries single `:` colons that are TS member/param
590
+ // separators, not Rip annotations. `inType` suppresses reclassification until
591
+ // the type ends (a terminator at its start depth, or exiting that depth).
592
+ let inType = false, typeStartDepth = 0;
593
+ const enterType = () => { inType = true; typeStartDepth = stack.length; };
594
+
595
+ // Object-context detection for context D: a statement-start `name:` line that
596
+ // sits next to another `key:` line is an object member (`{a: A, b: B}`), not a
597
+ // bare typed declaration. `curLineKV` marks the current line as starting
598
+ // `name:`; committed to `prevSiblingKV` at the line's TERMINATOR; both reset
599
+ // at block boundaries.
600
+ let prevSiblingKV = false, curLineKV = false;
601
+
602
+ const stack = [];
603
+ for (let i = 0; i < tokens.length; i++) {
604
+ const tag = tokens[i][0];
605
+
606
+ // Class-body tracking (INDENT/OUTDENT). `class` arms the next INDENT as a
607
+ // class body; nested method/blocks push non-class indents. INDENT/OUTDENT
608
+ // at the type's start depth also end a type expression.
609
+ if (tag === 'CLASS' || tag === 'COMPONENT') { pendingBody = true; continue; }
610
+ if (tag === 'INDENT') { indentStack.push(pendingBody); pendingBody = false; prevSiblingKV = curLineKV = false; if (inType && stack.length <= typeStartDepth) inType = false; continue; }
611
+ if (tag === 'OUTDENT') { indentStack.pop(); prevSiblingKV = curLineKV = false; if (inType && stack.length <= typeStartDepth) inType = false; continue; }
612
+
613
+ // Brackets are always tracked (depth drives the type-context boundary).
614
+ // Inside a type, an opener is never a real parameter list (it's a
615
+ // function-type's params), so it pushes a plain frame.
616
+ if (isOpen(tag)) {
617
+ let kind = 'other';
618
+ if (!inType) {
619
+ if (tag === 'PARAM_START') kind = 'param';
620
+ else if ((tag === 'CALL_START' || tag === '(') && isDefParamStart(i)) kind = 'defparam';
621
+ }
622
+ stack.push({ kind, sawEq: false, sawType: false });
623
+ continue;
624
+ }
625
+ if (isClose(tag)) {
626
+ const f = stack.pop();
627
+ if (f?.kind === 'defparam') { (tokens[i].data ??= {}).isDefParamEnd = true; }
628
+ if (inType && stack.length < typeStartDepth) inType = false;
629
+ continue;
630
+ }
631
+
632
+ // A type annotation token (pre-existing `::`, or one created below) opens a
633
+ // type expression.
634
+ if (tag === 'TYPE_ANNOTATION') { if (!inType) enterType(); continue; }
635
+
636
+ // Inside a type: clear at the type's terminator (at its start depth);
637
+ // otherwise skip — never reclassify a colon that belongs to a type. `=>`
638
+ // does not terminate (it's a function-type arrow).
639
+ if (inType) {
640
+ if (stack.length <= typeStartDepth &&
641
+ (tag === '=' || tag === ',' || tag === 'TERMINATOR' || tag === '->' ||
642
+ BINDING_OPS.has(tag))) {
643
+ inType = false; // fall through; a terminator is never a `:` to reclassify
644
+ } else {
645
+ continue;
646
+ }
647
+ }
648
+
649
+ // Commit per-line key:value state at statement boundaries (depth 0).
650
+ if (tag === 'TERMINATOR' && stack.length === 0) { prevSiblingKV = curLineKV; curLineKV = false; }
651
+
652
+ if (tag === ':') {
653
+ const prev = tokens[i - 1];
654
+ const prevTag = prev?.[0];
655
+ // Mark this line as `name:`-shaped (object-member candidate) for D's
656
+ // adjacent-key detection.
657
+ if (stack.length === 0 && (prevTag === 'IDENTIFIER' || prevTag === 'PROPERTY') &&
658
+ atStatementStart(tokens[i - 2])) curLineKV = true;
659
+ // Return type: a `:` immediately after a closed param list. Arrow param
660
+ // lists close as PARAM_END; def param lists close as `)` or CALL_END
661
+ // (flagged `isDefParamEnd`).
662
+ if (prevTag === 'PARAM_END' ||
663
+ ((prevTag === ')' || prevTag === 'CALL_END') && prev.data?.isDefParamEnd)) {
664
+ tokens[i][0] = 'TYPE_ANNOTATION'; enterType(); continue;
665
+ }
666
+ // Parameterless def return type: `def foo: T`.
667
+ if ((prevTag === 'IDENTIFIER' || prevTag === 'PROPERTY') &&
668
+ tokens[i - 2]?.[0] === 'DEF') {
669
+ if (prevTag === 'PROPERTY') prev[0] = 'IDENTIFIER';
670
+ tokens[i][0] = 'TYPE_ANNOTATION'; enterType(); continue;
671
+ }
672
+ // Statement-level typed declaration (top level, name at statement start,
673
+ // binding operator before any arrow). Covers `@name: T := v` props too.
674
+ // An `@`-prefixed name stays PROPERTY (the `@`-prop / promoted-field
675
+ // machinery needs it); a plain name is retagged IDENTIFIER like `::`.
676
+ if (stack.length === 0 && nameAtStatementStart(i) &&
677
+ declBindsBeforeArrow(i + 1)) {
678
+ if (prevTag === 'PROPERTY' && tokens[i - 2]?.[0] !== '@') prev[0] = 'IDENTIFIER';
679
+ tokens[i][0] = 'TYPE_ANNOTATION'; enterType(); continue;
680
+ }
681
+ // Context B — function-type-valued declaration: `name: (…) => Ret = value`
682
+ // (statement start). The type carries a `=>` arrow, so the binding-before-
683
+ // arrow check above misses it. Scan for the first bracket-depth-0 `=`
684
+ // (only ()[]{} count) whose preceding slice is a complete type expression
685
+ // — the `=>` is then a function-TYPE arrow. Try-and-continue handles
686
+ // generic defaults `Foo<T = U>` (the inner `=` leaves `Foo<T` incomplete).
687
+ if (stack.length === 0 && nameAtStatementStart(i)) {
688
+ let depth = 0;
689
+ for (let j = i + 1; j < tokens.length; j++) {
690
+ const g = tokens[j][0];
691
+ if (isOpen(g)) depth++;
692
+ else if (isClose(g)) { if (depth === 0) break; depth--; }
693
+ else if (depth === 0) {
694
+ if (g === 'TERMINATOR' || g === 'INDENT' || g === 'OUTDENT') break;
695
+ if (g === '=' && isCompleteTypeExpr(tokens, i + 1, j)) {
696
+ if (prevTag === 'PROPERTY' && tokens[i - 2]?.[0] !== '@') prev[0] = 'IDENTIFIER';
697
+ tokens[i][0] = 'TYPE_ANNOTATION'; enterType(); break;
698
+ }
699
+ }
700
+ }
701
+ if (tokens[i][0] === 'TYPE_ANNOTATION') continue;
702
+ }
703
+ // Class/component-body typed field: a bare `name: T` (or `@name: T`)
704
+ // member with no initializer is a typed field unless its value is a
705
+ // method (`name: (…) -> …`).
706
+ if (inTypedBody() && stack.length === 0 && nameAtStatementStart(i) &&
707
+ !valueIsMethod(i + 1)) {
708
+ if (prevTag === 'PROPERTY' && tokens[i - 2]?.[0] !== '@') prev[0] = 'IDENTIFIER';
709
+ tokens[i][0] = 'TYPE_ANNOTATION'; enterType(); continue;
710
+ }
711
+ // Context D — bare typed declaration, no initializer: `name: T` at
712
+ // statement start, the line is NON-TAIL (not the last expression of its
713
+ // block — that would be an implicit-return object), the type slice is a
714
+ // complete type expression (rejects `name: build()` so side effects
715
+ // survive), and the same `name` is assigned later in the block (positive
716
+ // evidence it's a forward declaration, not a discarded object property).
717
+ if (stack.length === 0 && nameAtStatementStart(i) && tokens[i - 2]?.[0] !== '@') {
718
+ let depth = 0, end = -1;
719
+ for (let j = i + 1; j < tokens.length; j++) {
720
+ const g = tokens[j][0];
721
+ if (isOpen(g)) depth++;
722
+ else if (isClose(g)) { if (depth === 0) break; depth--; }
723
+ else if (depth === 0) {
724
+ if (g === 'TERMINATOR') { end = j; break; }
725
+ if (g === 'INDENT' || g === 'OUTDENT' || g === '=' || BINDING_OPS.has(g)) break;
726
+ }
727
+ }
728
+ const nk = tokens[end + 1];
729
+ const nextKV = nk && (nk[0] === 'IDENTIFIER' || nk[0] === 'PROPERTY') &&
730
+ tokens[end + 2]?.[0] === ':';
731
+ if (end > i + 1 && nk && nk[0] !== 'OUTDENT' &&
732
+ !prevSiblingKV && !nextKV &&
733
+ isCompleteTypeExpr(tokens, i + 1, end) &&
734
+ assignedLaterInBlock(tokens, end + 1, prev[1])) {
735
+ if (prevTag === 'PROPERTY') prev[0] = 'IDENTIFIER';
736
+ tokens[i][0] = 'TYPE_ANNOTATION'; enterType(); continue;
737
+ }
738
+ }
739
+ }
740
+
741
+ const f = stack[stack.length - 1];
742
+ if (f && (f.kind === 'param' || f.kind === 'defparam')) {
743
+ if (tag === ',') { f.sawEq = false; f.sawType = false; continue; }
744
+ if (tag === '=') { f.sawEq = true; continue; }
745
+ if (tag === ':' && !f.sawEq && !f.sawType) {
746
+ const prev = tokens[i - 1];
747
+ const prevTag = prev?.[0];
748
+ if (prevTag === 'PROPERTY' || prevTag === 'IDENTIFIER') {
749
+ if (prevTag === 'PROPERTY' && tokens[i - 2]?.[0] !== '@') prev[0] = 'IDENTIFIER';
750
+ tokens[i][0] = 'TYPE_ANNOTATION';
751
+ f.sawType = true;
752
+ enterType();
753
+ } else if (prevTag === '}' || prevTag === ']' || prevTag === 'INDEX_END') {
754
+ // Context A — external destructuring param type, TS-style:
755
+ // `({pat}: Type)` / `([pat]: Type)`. The `:` follows the ROOT pattern
756
+ // close (the stack top is this param frame, so the pattern's own
757
+ // brackets have already been popped — a nested pattern's `:` is seen
758
+ // while an inner frame is on top and stays a rename). In-pattern `:`
759
+ // therefore keeps its destructuring-rename meaning; the type lives
760
+ // outside the pattern, after `}`/`]`.
761
+ tokens[i][0] = 'TYPE_ANNOTATION';
762
+ f.sawType = true;
763
+ enterType();
764
+ }
765
+ }
766
+ }
767
+ }
768
+ }
769
+
315
770
  // Collect type expression tokens starting at position j, respecting brackets
316
- function collectTypeExpression(tokens, j) {
771
+ function collectTypeExpression(tokens, j, opts = {}) {
317
772
  let typeTokens = [];
318
773
  let depth = 0;
319
774
  let bracketStack = []; // tracks innermost open bracket: '{', '[', '(', '<'
@@ -365,6 +820,12 @@ function collectTypeExpression(tokens, j) {
365
820
 
366
821
  // Delimiters that end the type at depth 0
367
822
  if (depth === 0) {
823
+ // Arrow-return context: a depth-0 `=>` is the arrow OPERATOR, so it ends
824
+ // the return type (symmetric with `->` below). A function-type return
825
+ // must therefore be parenthesized as a whole — see the foot-gun guard in
826
+ // the TYPE_ANNOTATION handler. Everywhere else `=>` is a TS function-type
827
+ // arrow and is collected (e.g. `cb:: () => void`).
828
+ if (opts.stopAtFatArrow && tTag === '=>') break;
368
829
  // After =>, INDENT wraps the return type body — collect through OUTDENT
369
830
  if (tTag === 'INDENT' && typeTokens.length > 0 && typeTokens[typeTokens.length - 1][0] === '=>') {
370
831
  j++; // skip INDENT