rip-lang 3.1.1 → 3.2.0

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/docs/RIP-TYPES.md CHANGED
@@ -1,46 +1,19 @@
1
- # Optional Types in Rip
1
+ # Rip Types
2
2
 
3
- > **Type-Driven Development Without the Overhead**
3
+ > **Types describe what you mean. Code does what you say.**
4
4
 
5
- This specification defines Rip's optional, lightweight type system — a thin, compile-time-only layer that enables TypeScript-level type-driven development while preserving Rip's core philosophy: minimal syntax, high readability, zero runtime overhead.
5
+ Rip supports an optional, lightweight, expressive way to design software by
6
+ defining shapes, intent, and contracts up front — without enforcing types at
7
+ runtime or burdening the language with a full type system.
6
8
 
7
- ---
8
-
9
- ## Table of Contents
10
-
11
- 1. [Philosophy](#1-philosophy)
12
- 2. [Type Annotations](#2-type-annotations)
13
- 3. [Type Aliases](#3-type-aliases)
14
- 4. [Optionality Modifiers](#4-optionality-modifiers)
15
- 5. [Structural Types](#5-structural-types)
16
- 6. [Union Types](#6-union-types)
17
- 7. [Function Types](#7-function-types)
18
- 8. [Generic Types](#8-generic-types)
19
- 9. [Type Inference](#9-type-inference)
20
- 10. [Adoption Model](#10-adoption-model)
21
- 11. [Emission Strategy](#11-emission-strategy)
22
- 12. [Interfaces](#12-interfaces)
23
- 13. [Enums](#13-enums)
24
- 14. [Boundary Validation](#14-boundary-validation)
25
- 15. [Implementation Notes](#15-implementation-notes)
26
- 16. [Quick Reference](#16-quick-reference)
27
-
28
- ---
29
-
30
- # 1. Philosophy
31
-
32
- ## Core Principles
9
+ Types in Rip are:
33
10
 
34
- 1. **Types are additive, never required** — Rip code without types is valid Rip code
35
- 2. **All type syntax erases at runtime** — Zero performance cost
36
- 3. **Type syntax decorates existing constructs** — No new control flow or semantics
37
- 4. **No typechecker required in Rip** — Parse and preserve, don't enforce
38
- 5. **Emit TypeScript-compatible types** — Leverage existing tooling
39
- 6. **Prefer zero-runtime representations** — Unions over enums, types over classes
11
+ - **Optional** — Rip code without types is valid Rip code
12
+ - **Erased at runtime** — Zero performance cost
13
+ - **TypeScript-compatible** — Emit `.d.ts` files, leverage existing tooling
14
+ - **Editor-driven** — Autocompletion, hover info, refactoring support
40
15
 
41
- ## The Rip Way
42
-
43
- Types in Rip should feel like documentation that happens to be machine-readable:
16
+ Rip treats types as *design scaffolding*, not safety rails.
44
17
 
45
18
  ```coffee
46
19
  # Without types (valid)
@@ -52,41 +25,83 @@ def greet(name:: string):: string
52
25
  "Hello, #{name}!"
53
26
  ```
54
27
 
55
- Both compile to identical JavaScript. Types exist for developers and tools, not for the runtime.
28
+ Both compile to identical JavaScript.
56
29
 
57
- ## Static Typing as a Discipline, Not a Mandate
30
+ ---
58
31
 
59
- Rip's type system is designed for teams and projects that want:
32
+ ## Table of Contents
60
33
 
61
- - **IDE intelligence** — Autocompletion, hover info, refactoring support
62
- - **Documentation** Self-documenting function signatures
63
- - **Boundary safety** — Confidence at API and module boundaries
64
- - **Gradual adoption** — Add types where they matter, skip where they don't
34
+ 1. [Type Sigil Reference](#type-sigil-reference)
35
+ 2. [Type Annotations (`::`)](#type-annotations-)
36
+ 3. [Type Aliases (`::=`)](#type-aliases-)
37
+ 4. [Structural Types](#structural-types)
38
+ 5. [Optionality Modifiers](#optionality-modifiers)
39
+ 6. [Union Types](#union-types)
40
+ 7. [Function Types](#function-types)
41
+ 8. [Generic Types](#generic-types)
42
+ 9. [Interfaces](#interfaces)
43
+ 10. [Enums](#enums)
44
+ 11. [Type Inference](#type-inference)
45
+ 12. [Adoption Model](#adoption-model)
46
+ 13. [Emission Strategy](#emission-strategy)
47
+ 14. [Boundary Validation](#boundary-validation)
48
+ 15. [Editor-First Workflow](#editor-first-workflow)
49
+ 16. [What Rip Intentionally Does Not Do](#what-rip-intentionally-does-not-do)
50
+ 17. [Implementation Plan](#implementation-plan)
65
51
 
66
52
  ---
67
53
 
68
- # 2. Type Annotations
54
+ ## Type Sigil Reference
55
+
56
+ | Sigil | Meaning | Example |
57
+ |-------|---------|---------|
58
+ | `::` | Type annotation | `count:: number = 0` |
59
+ | `::=` | Type alias | `ID ::= number` |
60
+ | `?` | Optional value (`T \| undefined`) | `email:: string?` |
61
+ | `??` | Nullable value (`T \| null \| undefined`) | `middle:: string??` |
62
+ | `!` | Non-nullable (`NonNullable<T>`) | `id:: ID!` |
63
+ | `?:` | Optional property | `email?: string` |
64
+ | `\|` | Union member | `"a" \| "b" \| "c"` |
65
+ | `=>` | Function type arrow | `(a: number) => string` |
66
+ | `<T>` | Generic parameter | `Container<T>` |
69
67
 
70
- ## Syntax: `::`
68
+ This is the complete Rip Types sigil vocabulary.
71
69
 
72
- The double-colon `::` annotates types on variables, parameters, return values, and properties.
70
+ ---
71
+
72
+ ## Type Annotations (`::`)
73
+
74
+ The double-colon `::` annotates types on variables, parameters, return values,
75
+ and properties.
73
76
 
74
77
  ### Variables
75
78
 
76
79
  ```coffee
77
80
  count:: number = 0
78
81
  name:: string = "Rip"
79
- active:: boolean = true
80
82
  items:: string[] = []
81
83
  ```
82
84
 
83
- **Emits to TypeScript:**
85
+ **Emits:**
86
+
87
+ ```ts
88
+ let count: number; count = 0;
89
+ let name: string; name = "Rip";
90
+ let items: string[]; items = [];
91
+ ```
92
+
93
+ ### Constants
94
+
95
+ ```coffee
96
+ MAX_RETRIES:: number =! 3
97
+ API_URL:: string =! "https://api.example.com"
98
+ ```
99
+
100
+ **Emits:**
84
101
 
85
102
  ```ts
86
- let count: number = 0;
87
- let name: string = "Rip";
88
- let active: boolean = true;
89
- let items: string[] = [];
103
+ const MAX_RETRIES: number = 3; // =! emits const
104
+ const API_URL: string = "https://api.example.com";
90
105
  ```
91
106
 
92
107
  ### Function Parameters
@@ -131,18 +146,10 @@ def processUser(id:: number, options:: Options):: Result
131
146
  transform(user, options)
132
147
  ```
133
148
 
134
- ### Constants
135
-
136
- ```coffee
137
- MAX_RETRIES:: number =! 3
138
- API_URL:: string =! "https://api.example.com"
139
- ```
140
-
141
149
  **Emits:**
142
150
 
143
151
  ```ts
144
- const MAX_RETRIES: number = 3;
145
- const API_URL: string = "https://api.example.com";
152
+ function processUser(id: number, options: Options): Result { ... }
146
153
  ```
147
154
 
148
155
  ### Reactive State
@@ -150,24 +157,33 @@ const API_URL: string = "https://api.example.com";
150
157
  Types work with Rip's reactive operators:
151
158
 
152
159
  ```coffee
153
- count:: number := 0 # Reactive state with type
154
- doubled:: number ~= count * 2 # Computed with type
160
+ count:: number := 0
161
+ doubled:: number ~= count * 2
155
162
  ```
156
163
 
157
- ---
164
+ **Emits:**
158
165
 
159
- # 3. Type Aliases
166
+ ```ts
167
+ const count = __state<number>(0); // := emits const
168
+ const doubled = __computed<number>(() => count * 2); // ~= emits const
169
+ ```
160
170
 
161
- ## Syntax: `::=`
171
+ ---
162
172
 
163
- The `::=` operator declares a named type alias, mapping directly to TypeScript's `type X = ...`.
173
+ ## Type Aliases (`::=`)
164
174
 
165
- ### Simple Aliases
175
+ The `::=` operator declares a named type, mapping directly to TypeScript's
176
+ `type X = ...`.
166
177
 
167
178
  ```coffee
179
+ # Simple aliases
168
180
  ID ::= number
169
181
  Name ::= string
170
- Timestamp ::= number
182
+
183
+ # Complex types
184
+ UserID ::= number | string
185
+ Callback ::= (error:: Error?, data:: any) => void
186
+ Handler ::= (req:: Request, res:: Response) => Promise<void>
171
187
  ```
172
188
 
173
189
  **Emits:**
@@ -175,32 +191,100 @@ Timestamp ::= number
175
191
  ```ts
176
192
  type ID = number;
177
193
  type Name = string;
178
- type Timestamp = number;
194
+
195
+ type UserID = number | string;
196
+ type Callback = (error: Error | undefined, data: any) => void;
197
+ type Handler = (req: Request, res: Response) => Promise<void>;
179
198
  ```
180
199
 
181
- ### Complex Types
200
+ ---
201
+
202
+ ## Structural Types
203
+
204
+ Define object shapes using the `type` keyword followed by a block:
182
205
 
183
206
  ```coffee
184
- UserID ::= number | string
185
- Callback ::= (error:: Error?, data:: any) -> void
186
- Handler ::= (req:: Request, res:: Response) -> Promise<void>
207
+ User ::= type
208
+ id: number
209
+ name: string
210
+ email?: string
211
+ createdAt: Date
212
+
213
+ Config ::= type
214
+ host: string
215
+ port: number
216
+ ssl?: boolean
217
+ timeout?: number
187
218
  ```
188
219
 
189
220
  **Emits:**
190
221
 
191
222
  ```ts
192
- type UserID = number | string;
193
- type Callback = (error: Error | undefined, data: any) => void;
194
- type Handler = (req: Request, res: Response) => Promise<void>;
223
+ type User = {
224
+ id: number;
225
+ name: string;
226
+ email?: string;
227
+ createdAt: Date;
228
+ };
229
+
230
+ type Config = {
231
+ host: string;
232
+ port: number;
233
+ ssl?: boolean;
234
+ timeout?: number;
235
+ };
236
+ ```
237
+
238
+ ### Nesting, Readonly, and Index Signatures
239
+
240
+ ```coffee
241
+ Response ::= type
242
+ data: type
243
+ users: User[]
244
+ total: number
245
+ meta: type
246
+ page: number
247
+ limit: number
248
+
249
+ ImmutableConfig ::= type
250
+ readonly host: string
251
+ readonly port: number
252
+
253
+ Dictionary ::= type
254
+ [key: string]: any
255
+ ```
256
+
257
+ **Emits:**
258
+
259
+ ```ts
260
+ type Response = {
261
+ data: {
262
+ users: User[];
263
+ total: number;
264
+ };
265
+ meta: {
266
+ page: number;
267
+ limit: number;
268
+ };
269
+ };
270
+
271
+ type ImmutableConfig = {
272
+ readonly host: string;
273
+ readonly port: number;
274
+ };
275
+
276
+ type Dictionary = {
277
+ [key: string]: any;
278
+ };
195
279
  ```
196
280
 
197
281
  ---
198
282
 
199
- # 4. Optionality Modifiers
283
+ ## Optionality Modifiers
200
284
 
201
285
  Lightweight suffix operators that map directly to TypeScript unions.
202
286
 
203
- ## Optional: `T?`
287
+ ### Optional: `T?`
204
288
 
205
289
  Indicates a value may be undefined.
206
290
 
@@ -216,7 +300,7 @@ email: string | undefined
216
300
  callback: Function | undefined
217
301
  ```
218
302
 
219
- ## Nullable Optional: `T??`
303
+ ### Nullable Optional: `T??`
220
304
 
221
305
  Indicates a value may be null or undefined.
222
306
 
@@ -232,7 +316,7 @@ middle: string | null | undefined
232
316
  cache: Map<string, any> | null | undefined
233
317
  ```
234
318
 
235
- ## Non-Nullable: `T!`
319
+ ### Non-Nullable: `T!`
236
320
 
237
321
  Asserts a value is never null or undefined.
238
322
 
@@ -248,7 +332,7 @@ id: NonNullable<ID>
248
332
  user: NonNullable<User>
249
333
  ```
250
334
 
251
- ## In Function Signatures
335
+ ### In Function Signatures
252
336
 
253
337
  ```coffee
254
338
  def findUser(id:: number):: User?
@@ -261,38 +345,25 @@ def updateUser(id:: number, email:: string??):: boolean
261
345
  ...
262
346
  ```
263
347
 
264
- ## Optional Properties
265
-
266
- In object types, `?` after the property name makes it optional:
348
+ **Emits:**
267
349
 
268
- ```coffee
269
- User ::= type
270
- id: number
271
- name: string
272
- email?: string # Optional property
273
- phone?: string? # Optional property that can also be undefined when present
350
+ ```ts
351
+ function findUser(id: number): User | undefined { ... }
352
+ function getUser(id: number): NonNullable<User> { ... }
353
+ function updateUser(id: number, email: string | null | undefined): boolean { ... }
274
354
  ```
275
355
 
276
- ---
277
-
278
- # 5. Structural Types
356
+ ### Optional Properties
279
357
 
280
- ## Object Types with `type` Block
281
-
282
- Define structural types using the `type` keyword followed by a block:
358
+ In structural types, `?` after the property name makes it optional (the
359
+ property itself may be absent), distinct from value optionality:
283
360
 
284
361
  ```coffee
285
362
  User ::= type
286
363
  id: number
287
364
  name: string
288
- email?: string
289
- createdAt: Date
290
-
291
- Config ::= type
292
- host: string
293
- port: number
294
- ssl?: boolean
295
- timeout?: number
365
+ email?: string # Optional property — may be absent
366
+ phone?: string? # Optional property that can also be undefined when present
296
367
  ```
297
368
 
298
369
  **Emits:**
@@ -302,93 +373,32 @@ type User = {
302
373
  id: number;
303
374
  name: string;
304
375
  email?: string;
305
- createdAt: Date;
306
- };
307
-
308
- type Config = {
309
- host: string;
310
- port: number;
311
- ssl?: boolean;
312
- timeout?: number;
313
- };
314
- ```
315
-
316
- ## Nested Types
317
-
318
- ```coffee
319
- Response ::= type
320
- data: type
321
- users: User[]
322
- total: number
323
- meta: type
324
- page: number
325
- limit: number
326
- ```
327
-
328
- **Emits:**
329
-
330
- ```ts
331
- type Response = {
332
- data: {
333
- users: User[];
334
- total: number;
335
- };
336
- meta: {
337
- page: number;
338
- limit: number;
339
- };
376
+ phone?: string | undefined;
340
377
  };
341
378
  ```
342
379
 
343
- ## Array Properties
344
-
345
- ```coffee
346
- Collection ::= type
347
- items: Item[]
348
- tags: string[]
349
- matrix: number[][]
350
- ```
351
-
352
- ## Readonly Properties
380
+ ### Key Distinction
353
381
 
354
- ```coffee
355
- ImmutableConfig ::= type
356
- readonly host: string
357
- readonly port: number
358
- ```
359
-
360
- ## Index Signatures
361
-
362
- ```coffee
363
- Dictionary ::= type
364
- [key: string]: any
365
-
366
- StringMap ::= type
367
- [key: string]: string
368
- ```
382
+ - `email?: string` — property may be absent
383
+ - `email:: string?` — value may be undefined
384
+ - `email?: string??` — property may be absent, value may be null or undefined
369
385
 
370
386
  ---
371
387
 
372
- # 6. Union Types
388
+ ## Union Types
373
389
 
374
- ## Inline Unions
390
+ ### Inline
375
391
 
376
392
  ```coffee
377
393
  Status ::= "pending" | "active" | "done"
378
394
  Result ::= Success | Error
379
- ID ::= number | string
380
395
  ```
381
396
 
382
- ## Block Unions (Preferred)
397
+ ### Block Form (Preferred)
383
398
 
384
- For readability and diff-friendliness, use the block form with leading `|`:
399
+ Vertical form is diff-friendly and encourages domain-first modeling:
385
400
 
386
401
  ```coffee
387
- Status ::=
388
- | "pending"
389
- | "active"
390
- | "done"
391
-
392
402
  HttpMethod ::=
393
403
  | "GET"
394
404
  | "POST"
@@ -413,105 +423,66 @@ type Result =
413
423
  | { success: false; error: Error };
414
424
  ```
415
425
 
416
- ## Why Block Unions Over Enums
417
-
418
- Block unions have **zero runtime cost** — they exist only at compile time. Traditional enums generate runtime code:
419
-
420
- ```coffee
421
- # Preferred: Zero runtime cost
422
- Size ::=
423
- | "xs"
424
- | "sm"
425
- | "md"
426
- | "lg"
427
-
428
- # Avoid: Generates runtime object
429
- enum Size
430
- xs
431
- sm
432
- md
433
- lg
434
- ```
426
+ ### Why Unions Over Enums
435
427
 
436
- Use enums only when you need:
437
- - Reverse mapping (value name)
438
- - Runtime iteration over values
439
- - Explicit numeric values
428
+ Block unions have **zero runtime cost** — they exist only at compile time.
429
+ Use enums only when you need reverse mapping, runtime iteration, or explicit
430
+ numeric values.
440
431
 
441
432
  ---
442
433
 
443
- # 7. Function Types
444
-
445
- ## Arrow Function Types
434
+ ## Function Types
446
435
 
447
436
  ```coffee
448
- Comparator ::= (a:: any, b:: any) -> number
449
- AsyncFetcher ::= (url:: string) -> Promise<Response>
450
- Callback ::= (err:: Error?, data:: any?) -> void
451
- ```
452
-
453
- **Emits:**
454
-
455
- ```ts
456
- type Comparator = (a: any, b: any) => number;
457
- type AsyncFetcher = (url: string) => Promise<Response>;
458
- type Callback = (err: Error | undefined, data: any | undefined) => void;
459
- ```
437
+ # Type aliases for function signatures
438
+ Comparator ::= (a:: any, b:: any) => number
439
+ AsyncFetcher ::= (url:: string) => Promise<Response>
460
440
 
461
- ## Function Overloads
462
-
463
- Multiple signatures for a single implementation:
464
-
465
- ```coffee
466
- # Overload signatures
441
+ # Overloads
467
442
  def toHtml(content:: string):: string
468
443
  def toHtml(nodes:: Element[]):: string
469
- def toHtml(fragment:: DocumentFragment):: string
470
-
471
- # Implementation (matches last signature or uses widest type)
472
444
  def toHtml(input:: any):: string
473
445
  switch typeof input
474
446
  when "string" then escapeHtml(input)
475
447
  else renderNodes(input)
448
+
449
+ # Method signatures in classes
450
+ class UserService
451
+ find: (id:: number):: User? ->
452
+ @db.find(id)
453
+
454
+ create: (data:: CreateUserInput):: User ->
455
+ @db.create(data)
476
456
  ```
477
457
 
478
458
  **Emits:**
479
459
 
480
460
  ```ts
461
+ type Comparator = (a: any, b: any) => number;
462
+ type AsyncFetcher = (url: string) => Promise<Response>;
463
+
481
464
  function toHtml(content: string): string;
482
465
  function toHtml(nodes: Element[]): string;
483
- function toHtml(fragment: DocumentFragment): string;
484
466
  function toHtml(input: any): string {
485
467
  // implementation
486
468
  }
487
- ```
488
-
489
- ## Method Signatures in Classes
490
469
 
491
- ```coffee
492
- class UserService
493
- find: (id:: number):: User? ->
494
- @db.find(id)
495
-
496
- create: (data:: CreateUserInput):: User ->
497
- @db.create(data)
498
-
499
- update: (id:: number, data:: UpdateUserInput):: User? ->
500
- @db.update(id, data)
470
+ class UserService {
471
+ find(id: number): User | undefined { ... }
472
+ create(data: CreateUserInput): User { ... }
473
+ }
501
474
  ```
502
475
 
503
476
  ---
504
477
 
505
- # 8. Generic Types
506
-
507
- ## Generic Type Parameters
478
+ ## Generic Types
508
479
 
509
480
  ```coffee
510
481
  # Simple generic
511
482
  Container<T> ::= type
512
483
  value: T
513
484
 
514
- # Multiple type parameters
485
+ # Multiple parameters
515
486
  Pair<K, V> ::= type
516
487
  key: K
517
488
  value: V
@@ -519,7 +490,17 @@ Pair<K, V> ::= type
519
490
  # With constraints
520
491
  Comparable<T extends Ordered> ::= type
521
492
  value: T
522
- compareTo: (other:: T) -> number
493
+ compareTo: (other:: T) => number
494
+
495
+ # Generic functions
496
+ def identity<T>(value:: T):: T
497
+ value
498
+
499
+ def map<T, U>(items:: T[], fn:: (item:: T) => U):: U[]
500
+ items.map(fn)
501
+
502
+ def merge<T extends object, U extends object>(a:: T, b:: U):: T & U
503
+ {...a, ...b}
523
504
  ```
524
505
 
525
506
  **Emits:**
@@ -538,65 +519,118 @@ type Comparable<T extends Ordered> = {
538
519
  value: T;
539
520
  compareTo: (other: T) => number;
540
521
  };
522
+
523
+ function identity<T>(value: T): T { ... }
524
+ function map<T, U>(items: T[], fn: (item: T) => U): U[] { ... }
525
+ function merge<T extends object, U extends object>(a: T, b: U): T & U { ... }
541
526
  ```
542
527
 
543
- ## Generic Functions
528
+ ---
544
529
 
545
- ```coffee
546
- def identity<T>(value:: T):: T
547
- value
530
+ ## Interfaces
548
531
 
549
- def map<T, U>(items:: T[], fn:: (item:: T) -> U):: U[]
550
- items.map(fn)
532
+ For TypeScript compatibility, Rip supports the `interface` keyword:
551
533
 
552
- def first<T>(items:: T[]):: T?
553
- items[0]
534
+ ```coffee
535
+ interface Animal
536
+ name: string
537
+
538
+ interface Dog extends Animal
539
+ breed: string
540
+ bark: () => void
554
541
  ```
555
542
 
556
- ## Generic Constraints
543
+ **Emits:**
557
544
 
558
- ```coffee
559
- def merge<T extends object, U extends object>(a:: T, b:: U):: T & U
560
- {...a, ...b}
545
+ ```ts
546
+ interface Animal {
547
+ name: string;
548
+ }
561
549
 
562
- def stringify<T extends { toString: () -> string }>(value:: T):: string
563
- value.toString()
550
+ interface Dog extends Animal {
551
+ breed: string;
552
+ bark: () => void;
553
+ }
564
554
  ```
565
555
 
556
+ Use `::= type` by default. Use `interface` when you need declaration merging.
557
+
566
558
  ---
567
559
 
568
- # 9. Type Inference
560
+ ## Enums
561
+
562
+ ### Zero-Runtime (Preferred)
563
+
564
+ ```coffee
565
+ Size ::=
566
+ | "xs"
567
+ | "sm"
568
+ | "md"
569
+ | "lg"
570
+ | "xl"
571
+ ```
572
+
573
+ **Emits to `.d.ts` only:**
574
+
575
+ ```ts
576
+ type Size = "xs" | "sm" | "md" | "lg" | "xl";
577
+ ```
578
+
579
+ ### Runtime Enums (When Needed)
580
+
581
+ ```coffee
582
+ enum HttpCode
583
+ ok = 200
584
+ created = 201
585
+ notFound = 404
586
+ serverError = 500
587
+ ```
588
+
589
+ **Emits to both `.js` and `.d.ts`:**
590
+
591
+ ```ts
592
+ // .d.ts
593
+ enum HttpCode {
594
+ ok = 200,
595
+ created = 201,
596
+ notFound = 404,
597
+ serverError = 500
598
+ }
599
+
600
+ // .js (runtime object with reverse mapping)
601
+ const HttpCode = {
602
+ ok: 200, created: 201, notFound: 404, serverError: 500,
603
+ 200: "ok", 201: "created", 404: "notFound", 500: "serverError"
604
+ };
605
+ ```
569
606
 
570
- ## When to Annotate
607
+ Runtime enums generate a reverse-mapping object. Use only when you need
608
+ `HttpCode[200]` → `"ok"` or runtime iteration.
609
+
610
+ ---
571
611
 
572
- Rip's type system uses **TypeScript-compatible inference**. Types should be explicit at boundaries, optional elsewhere:
612
+ ## Type Inference
573
613
 
574
- ### Explicit (Recommended)
614
+ Types should be explicit at boundaries, optional elsewhere:
575
615
 
576
616
  ```coffee
577
- # Function signatures always annotate
578
- def processUser(id:: number, options:: ProcessOptions):: Result
617
+ # Explicit — function signatures, exports, class properties
618
+ def processUser(id:: number, options:: Options):: Result
579
619
  ...
580
620
 
581
- # Exported values — always annotate
582
621
  export config:: Config = loadConfig()
583
622
 
584
- # Class properties — annotate for clarity
585
623
  class UserService
586
624
  db:: Database
587
625
  cache:: Map<string, User>
588
- ```
589
-
590
- ### Inferred (Acceptable)
591
626
 
592
- ```coffee
593
- # Local variables let TypeScript infer
594
- user = getUser!(id) # Inferred from getUser's return type
627
+ # Inferred — local variables
628
+ user = getUser!(id) # Inferred from return type
595
629
  items = users.map (u) -> u.name # Inferred as string[]
596
- count = 0 # Inferred as number
630
+ count = 0 # Inferred as number
597
631
  ```
598
632
 
599
- ## Inference Rules
633
+ ### Inference Rules
600
634
 
601
635
  1. **Function returns** — Inferred from body if not annotated
602
636
  2. **Variables** — Inferred from initializer
@@ -605,13 +639,11 @@ count = 0 # Inferred as number
605
639
 
606
640
  ---
607
641
 
608
- # 10. Adoption Model
609
-
610
- Types are optional at three levels: project, file, and line.
642
+ ## Adoption Model
611
643
 
612
- ## Project Level
644
+ Types are optional at every level:
613
645
 
614
- Configure in `package.json` or `rip.config.json`:
646
+ ### Project Level
615
647
 
616
648
  ```json
617
649
  {
@@ -623,38 +655,21 @@ Configure in `package.json` or `rip.config.json`:
623
655
 
624
656
  | Mode | Behavior |
625
657
  |------|----------|
626
- | `"off"` | Ignore all type syntax (strip during parse) |
627
- | `"emit"` | Parse types, emit `.d.ts` files |
628
- | `"check"` | Parse types, emit `.d.ts`, run `tsc --noEmit` |
629
-
630
- ## File Level
631
-
632
- Override project settings per-file with a directive comment:
633
-
634
- ```coffee
635
- # @types off
636
- # This file has no type checking
658
+ | `"off"` | Types are parsed but ignored no `.d.ts` emitted |
659
+ | `"emit"` | `.d.ts` files generated — enables editor IntelliSense |
660
+ | `"check"` | `.d.ts` generated + `tsc --noEmit` validates types |
637
661
 
638
- # @types emit
639
- # Types parsed and emitted for this file
662
+ ### File Level
640
663
 
641
- # @types check
642
- # Types checked via TypeScript for this file
643
- ```
644
-
645
- ## Line Level
646
-
647
- All type syntax is simply ignored if types are disabled. No special syntax needed — annotations silently disappear:
664
+ Override per-file with a directive:
648
665
 
649
666
  ```coffee
650
- # With types enabled:
651
- count:: number = 0 # Type preserved
652
-
653
- # With types disabled:
654
- count:: number = 0 # Parses as: count = 0
667
+ # @types off — Ignore types in this file
668
+ # @types emit — Parse and emit .d.ts
669
+ # @types check — Full TypeScript checking
655
670
  ```
656
671
 
657
- ## Gradual Adoption Path
672
+ ### Gradual Path
658
673
 
659
674
  1. **Start with `"off"`** — Write normal Rip code
660
675
  2. **Enable `"emit"`** — Add types where helpful, get `.d.ts` for tooling
@@ -662,18 +677,16 @@ count:: number = 0 # Parses as: count = 0
662
677
 
663
678
  ---
664
679
 
665
- # 11. Emission Strategy
666
-
667
- ## Compilation Outputs
680
+ ## Emission Strategy
668
681
 
669
682
  | Input | Output | Purpose |
670
683
  |-------|--------|---------|
671
684
  | `file.rip` | `file.js` | Runtime code (always) |
672
685
  | `file.rip` | `file.d.ts` | Type declarations (when `emit` or `check`) |
673
686
 
674
- ## Type Declaration Files
675
-
676
- When `types: "emit"` or `types: "check"`, generate `.d.ts` files:
687
+ Type aliases and annotations are stripped from `.js` output and preserved
688
+ in `.d.ts` output. Type-only declarations (`::=` aliases, unions) appear
689
+ only in `.d.ts`.
677
690
 
678
691
  ```coffee
679
692
  # user.rip
@@ -685,18 +698,7 @@ export def getUser(id:: number):: User?
685
698
  db.find(id)
686
699
  ```
687
700
 
688
- **Generates `user.d.ts`:**
689
-
690
- ```ts
691
- export type User = {
692
- id: number;
693
- name: string;
694
- };
695
-
696
- export function getUser(id: number): User | undefined;
697
- ```
698
-
699
- **Generates `user.js`:**
701
+ **Generates `user.js`** — types erased:
700
702
 
701
703
  ```js
702
704
  export function getUser(id) {
@@ -704,426 +706,1174 @@ export function getUser(id) {
704
706
  }
705
707
  ```
706
708
 
707
- ## Type-Only Exports
709
+ **Generates `user.d.ts`** — types preserved:
708
710
 
709
- Type aliases that have no runtime representation are only emitted to `.d.ts`:
711
+ ```ts
712
+ export type User = { id: number; name: string; };
713
+ export function getUser(id: number): User | undefined;
714
+ ```
715
+
716
+ ---
717
+
718
+ ## Boundary Validation
719
+
720
+ Types are compile-time contracts. For data entering your system, validate
721
+ at boundaries:
710
722
 
711
723
  ```coffee
712
- # These only appear in .d.ts, not .js
713
- Status ::= "pending" | "active" | "done"
714
- UserID ::= number
724
+ import { z } from "zod"
725
+
726
+ # Define schema (runtime validation + type source of truth)
727
+ UserSchema = z.object
728
+ id: z.number()
729
+ name: z.string()
730
+ email: z.string().email()
731
+
732
+ # Derive type from schema
733
+ User ::= z.infer<typeof UserSchema>
734
+
735
+ # Validate at API boundary
736
+ def createUser(req:: Request):: User
737
+ data = req.json!
738
+ UserSchema.parse(data)
715
739
  ```
716
740
 
717
- ## Integration with TypeScript
741
+ Once data passes boundary validation, trust the types internally — no need
742
+ to re-validate.
743
+
744
+ ---
745
+
746
+ ## Editor-First Workflow
718
747
 
719
- When `types: "check"`:
748
+ Rip Types is primarily **editor-driven**. The intended loop:
720
749
 
721
- 1. Compile `.rip` `.js` + `.d.ts`
722
- 2. Run `tsc --noEmit` to validate types
723
- 3. Report TypeScript errors
750
+ 1. Define shapes and contracts
751
+ 2. Annotate public boundaries
752
+ 3. Let the editor guide implementation
753
+ 4. Optionally validate via `types: "check"`
724
754
 
725
- This leverages TypeScript's mature type checker without reimplementing it.
755
+ High-quality `.d.ts` output is a first-class goal.
726
756
 
727
757
  ---
728
758
 
729
- # 12. Interfaces
759
+ ## What Rip Intentionally Does Not Do
730
760
 
731
- ## Syntax: `interface`
761
+ Rip does not:
732
762
 
733
- For TypeScript compatibility, Rip also supports the `interface` keyword for object types:
763
+ - Narrow types based on control flow
764
+ - Perform exhaustiveness checks
765
+ - Reject programs based on type errors
766
+ - Introduce runtime type checks
767
+ - Evaluate type expressions or prove soundness
734
768
 
735
- ```coffee
736
- interface User
737
- id: number
738
- name: string
739
- email?: string
740
- ```
769
+ These responsibilities belong to editors, linters, and TypeScript tooling
770
+ (when enabled via `types: "check"`).
741
771
 
742
- **Emits:**
772
+ Rip only needs to:
743
773
 
744
- ```ts
745
- interface User {
746
- id: number;
747
- name: string;
748
- email?: string;
749
- }
750
- ```
774
+ - **Parse** type syntax correctly
775
+ - **Preserve** type information in the AST
776
+ - **Emit** valid TypeScript type declarations
751
777
 
752
- ## Type vs Interface
778
+ ---
753
779
 
754
- | Feature | `::= type` | `interface` |
755
- |---------|------------|-------------|
756
- | Declaration merging | No | Yes |
757
- | Extends | Via `&` intersection | Via `extends` |
758
- | Computed properties | Yes | No |
759
- | Mapped types | Yes | No |
780
+ ## Summary
760
781
 
761
- **Recommendation:** Use `::= type` by default. Use `interface` when you need declaration merging or prefer the interface aesthetic.
782
+ Rip Types provides:
762
783
 
763
- ## Interface Extension
784
+ - Expressive domain modeling with minimal syntax
785
+ - Gradual adoption — add types where they help
786
+ - Zero runtime overhead — types are compile-time only
787
+ - Excellent editor intelligence via `.d.ts` emission
788
+ - Clean interop with the TypeScript ecosystem
764
789
 
765
- ```coffee
766
- interface Animal
767
- name: string
790
+ Rip remains a **JavaScript language**, with types as a **design language**
791
+ layered on top.
768
792
 
769
- interface Dog extends Animal
770
- breed: string
771
- bark: () -> void
772
- ```
793
+ > **Static typing as a discipline, not a mandate.**
773
794
 
774
795
  ---
775
796
 
776
- # 13. Enums
797
+ ## Implementation Plan
777
798
 
778
- ## Zero-Runtime Enums (Preferred)
799
+ This section describes how to implement Rip Types in the compiler. It is
800
+ designed to be self-contained — an implementor should be able to read this
801
+ section and the referenced source files and complete the work in one pass.
779
802
 
780
- Use string literal unions for zero runtime cost:
803
+ > **Interoperability Principle.** Rip Types emits standard TypeScript `.d.ts`
804
+ > declaration files — the same format any TypeScript project produces. The
805
+ > goal is full ecosystem interoperability. A Rip library's types are
806
+ > indistinguishable from hand-written TypeScript to any consumer. Rip
807
+ > developers get IDE autocompletion and type checking for third-party
808
+ > packages (React, Express, etc.) through the TypeScript Language Server,
809
+ > which reads standard `.d.ts` files from `node_modules`. Rip is a
810
+ > participant in the TypeScript ecosystem, not a replacement for it.
781
811
 
782
- ```coffee
783
- Size ::=
784
- | "xs"
785
- | "sm"
786
- | "md"
787
- | "lg"
788
- | "xl"
812
+ ### File Architecture
813
+
814
+ Rip Types follows the same sidecar pattern as the component system.
815
+ Components added `src/components.js` alongside the compiler — types add
816
+ `src/types.js` alongside the lexer:
817
+
818
+ | File | Role | Scope |
819
+ |------|------|-------|
820
+ | `src/lexer.js` | Detect `::` and `::=` tokens, import `installTypeSupport` from `types.js` | Small inline changes |
821
+ | `src/types.js` | Lexer sidecar: `installTypeSupport(Lexer)`, `emitTypes(tokens)`, `generateEnum()` | New file, bulk of logic |
822
+ | `src/compiler.js` | Call `emitTypes()` before parsing, wire `generateEnum()` | ~8 lines |
823
+ | `src/grammar/grammar.rip` | Add `Enum` rule to `Expression` | 1 rule + 1 export |
824
+
825
+ The boundary is the token stream. Types are fully resolved before parsing —
826
+ `rewriteTypes()` strips annotations, `emitTypes()` produces `.d.ts` from
827
+ annotated tokens, and type-only constructs are removed before the parser
828
+ runs. Only `enum` crosses into the parser/compiler because it generates
829
+ runtime JavaScript.
830
+
831
+ **The `components.js` pattern to follow:**
832
+
833
+ ```js
834
+ // src/components.js — existing sidecar for the compiler
835
+ export function installComponentSupport(CodeGenerator) {
836
+ const proto = CodeGenerator.prototype;
837
+ proto.generateComponent = function(head, rest, context) { ... };
838
+ proto.generateRender = function(head, rest, context, sexpr) { ... };
839
+ proto.buildRender = function(body) { ... };
840
+ // ...
841
+ }
789
842
 
790
- Direction ::=
791
- | "north"
792
- | "south"
793
- | "east"
794
- | "west"
843
+ // src/compiler.js — existing wiring
844
+ import { installComponentSupport } from './components.js';
845
+ installComponentSupport(CodeGenerator); // at module level, after class definition
795
846
  ```
796
847
 
797
- **Emits only to `.d.ts`:**
848
+ **The parallel for types:**
798
849
 
799
- ```ts
800
- type Size = "xs" | "sm" | "md" | "lg" | "xl";
801
- type Direction = "north" | "south" | "east" | "west";
850
+ ```js
851
+ // src/types.js new sidecar for the lexer
852
+ export function installTypeSupport(Lexer) {
853
+ Lexer.prototype.rewriteTypes = function() { ... }; // Uses this.scanTokens()
854
+ }
855
+ export function emitTypes(tokens) { ... } // Annotated tokens -> .d.ts string
856
+ export function generateEnum(head, rest, ctx) { ... } // Enum -> runtime JS object
857
+
858
+ // src/lexer.js — wiring (mirrors compiler.js wiring for components)
859
+ import { installTypeSupport } from './types.js';
860
+ installTypeSupport(Lexer);
861
+
862
+ // src/compiler.js — wiring
863
+ import { emitTypes, generateEnum } from './types.js';
864
+ CodeGenerator.prototype.generateEnum = generateEnum;
865
+ CodeGenerator.GENERATORS['enum'] = 'generateEnum';
866
+ // In compile(): let dts = emitTypes(tokens);
802
867
  ```
803
868
 
804
- ## Runtime Enums (When Needed)
869
+ ### Phase 1: Type Annotations (Metadata on Existing Tokens)
805
870
 
806
- When you need runtime access to enum values, use the `enum` keyword:
871
+ Type annotations on variables, parameters, return types, and reactive state
872
+ ride as **metadata** on existing tokens. The grammar never sees them. This
873
+ means all existing grammar rules, s-expression shapes, and codegen work
874
+ unchanged — types are invisible to everything except `rewriteTypes()` and
875
+ `emitTypes()`.
807
876
 
808
- ```coffee
809
- enum Status
810
- pending
811
- active
812
- completed
813
- cancelled
877
+ #### 1.1 Lexer: Token Detection
814
878
 
815
- enum HttpCode
816
- ok = 200
817
- created = 201
818
- badRequest = 400
819
- notFound = 404
820
- serverError = 500
879
+ **Add `::=` and `::` to `OPERATOR_RE`** (in `src/lexer.js`, near line 215).
880
+
881
+ The current regex:
882
+
883
+ ```js
884
+ let OPERATOR_RE = /^(?:<=>|[-=]>|~>|~=|:=|=!|===|!==|...)/;
821
885
  ```
822
886
 
823
- **Emits to both `.js` and `.d.ts`:**
887
+ Add `::=` and `::` with longest-match-first ordering (`::=` before `::`).
888
+ Also note that `IDENTIFIER_RE` already avoids matching `:` when followed by
889
+ `=` or `:` (the `(?![=:])` lookahead), so `::` after an identifier will fall
890
+ through to `literalToken()` where `OPERATOR_RE` picks it up.
824
891
 
825
- ```ts
826
- // .d.ts
827
- enum Status {
828
- pending,
829
- active,
830
- completed,
831
- cancelled
892
+ **Tag the operators** (in the operator tagging section of `literalToken()`,
893
+ near lines 1095-1100, alongside the reactive operators):
894
+
895
+ ```js
896
+ else if (val === '::=') tag = 'TYPE_ALIAS';
897
+ else if (val === '::') tag = 'TYPE_ANNOTATION';
898
+ ```
899
+
900
+ These must be checked **before** the `([-+:])\1` pattern in the regex, which
901
+ currently matches `::` as a repeated `:`. The `::=` three-character match must
902
+ come first.
903
+
904
+ #### 1.2 Rewriter: `rewriteTypes()`
905
+
906
+ **Add the pass to the rewrite pipeline** (in the `rewrite()` method, after
907
+ `rewriteRender()`):
908
+
909
+ ```js
910
+ rewrite(tokens) {
911
+ this.tokens = tokens;
912
+ this.removeLeadingNewlines();
913
+ this.closeOpenCalls();
914
+ this.closeOpenIndexes();
915
+ this.normalizeLines();
916
+ this.rewriteRender();
917
+ this.rewriteTypes(); // <-- NEW PASS
918
+ this.tagPostfixConditionals();
919
+ this.addImplicitBracesAndParens();
920
+ this.addImplicitCallCommas();
921
+ return this.tokens;
832
922
  }
923
+ ```
833
924
 
834
- // .js (runtime object generated)
835
- const Status = {
836
- pending: 0,
837
- active: 1,
838
- completed: 2,
839
- cancelled: 3,
840
- 0: "pending",
841
- 1: "active",
842
- 2: "completed",
843
- 3: "cancelled"
844
- };
925
+ **The `rewriteTypes()` method** scans the token stream using `scanTokens()`.
926
+ When it encounters a `TYPE_ANNOTATION` (`::`) token, it:
927
+
928
+ 1. Collects all following tokens that are part of the type expression
929
+ 2. Joins them into a type string
930
+ 3. Finds the token that survives into the s-expression (see §1.4)
931
+ 4. Stores the type string on that token (`.data.type` or `.data.returnType`)
932
+ 5. Removes the `::` and collected type tokens from the stream
933
+
934
+ ```js
935
+ rewriteTypes() {
936
+ this.scanTokens((token, i, tokens) => {
937
+ let tag = token[0];
938
+
939
+ // --- Handle :: (type annotations) ---
940
+ if (tag === 'TYPE_ANNOTATION') {
941
+ let prevToken = tokens[i - 1];
942
+ if (!prevToken) return 1;
943
+
944
+ // Collect the type expression
945
+ let typeTokens = [];
946
+ let j = i + 1;
947
+ let depth = 0;
948
+
949
+ while (j < tokens.length) {
950
+ let t = tokens[j];
951
+ let tTag = t[0];
952
+
953
+ // Bracket balancing
954
+ // NOTE: The lexer tags < and > as 'COMPARE', not '<' or '>'
955
+ let isOpen = tTag === '(' || tTag === '[' ||
956
+ tTag === 'CALL_START' || tTag === 'PARAM_START' || tTag === 'INDEX_START' ||
957
+ (tTag === 'COMPARE' && t[1] === '<');
958
+ let isClose = tTag === ')' || tTag === ']' ||
959
+ tTag === 'CALL_END' || tTag === 'PARAM_END' || tTag === 'INDEX_END' ||
960
+ (tTag === 'COMPARE' && t[1] === '>');
961
+
962
+ if (isOpen) {
963
+ depth++;
964
+ typeTokens.push(t);
965
+ j++;
966
+ continue;
967
+ }
968
+ if (isClose) {
969
+ if (depth > 0) {
970
+ depth--;
971
+ typeTokens.push(t);
972
+ j++;
973
+ continue;
974
+ }
975
+ break; // Unbalanced close at depth 0 — end of type
976
+ }
977
+
978
+ // Delimiters that end the type at depth 0
979
+ if (depth === 0) {
980
+ if (tTag === '=' || tTag === 'REACTIVE_ASSIGN' ||
981
+ tTag === 'COMPUTED_ASSIGN' || tTag === 'READONLY_ASSIGN' ||
982
+ tTag === 'REACT_ASSIGN' || tTag === 'TERMINATOR' ||
983
+ tTag === 'INDENT' || tTag === 'OUTDENT' ||
984
+ tTag === '->') { // code arrow ends type
985
+ break;
986
+ }
987
+ if (tTag === ',') break;
988
+ }
989
+
990
+ // => at depth 0: function type arrow, type continues
991
+ // -> at depth 0: code arrow, already handled as delimiter above
992
+ typeTokens.push(t);
993
+ j++;
994
+ }
995
+
996
+ // Build type string from collected tokens
997
+ let typeStr = typeTokens.map(t => t[1]).join(' ').replace(/\s+/g, ' ').trim();
998
+ // Clean up spacing around brackets
999
+ typeStr = typeStr
1000
+ .replace(/\s*<\s*/g, '<').replace(/\s*>\s*/g, '>')
1001
+ .replace(/\s*\[\s*/g, '[').replace(/\s*\]\s*/g, ']')
1002
+ .replace(/\s*\(\s*/g, '(').replace(/\s*\)\s*/g, ')')
1003
+ .replace(/\s*,\s*/g, ', ');
1004
+
1005
+ // Attach type to the right target token
1006
+ //
1007
+ // IMPORTANT: For return types, the preceding token is CALL_END or
1008
+ // PARAM_END, but the grammar discards these tokens (they don't appear
1009
+ // in s-expressions). Instead, find the token that DOES survive:
1010
+ // CALL_END → scan backward to function name IDENTIFIER
1011
+ // PARAM_END → scan forward to the -> token
1012
+ //
1013
+ let target = prevToken;
1014
+ let propName = 'type';
1015
+
1016
+ if (prevToken[0] === 'CALL_END' || prevToken[0] === ')') {
1017
+ // Return type on DEF with parameters.
1018
+ // Scan backward past balanced parens to find function name.
1019
+ let d = 1, k = i - 2;
1020
+ while (k >= 0 && d > 0) {
1021
+ let kTag = tokens[k][0];
1022
+ if (kTag === 'CALL_END' || kTag === ')') d++;
1023
+ if (kTag === 'CALL_START' || kTag === '(') d--;
1024
+ k--;
1025
+ }
1026
+ // k is now at the token before CALL_START — the function name
1027
+ if (k >= 0) target = tokens[k];
1028
+ propName = 'returnType';
1029
+ } else if (prevToken[0] === 'PARAM_END') {
1030
+ // Return type on arrow function: (x:: number):: string -> ...
1031
+ // Scan forward past the type tokens to find the -> token.
1032
+ let arrowIdx = i + 1 + typeTokens.length;
1033
+ let arrowToken = tokens[arrowIdx];
1034
+ if (arrowToken && (arrowToken[0] === '->' || arrowToken[0] === '=>')) {
1035
+ target = arrowToken;
1036
+ }
1037
+ propName = 'returnType';
1038
+ } else if (prevToken[0] === 'IDENTIFIER' && i >= 2 &&
1039
+ tokens[i - 2]?.[0] === 'DEF') {
1040
+ // Return type on parameterless function: def foo:: string
1041
+ propName = 'returnType';
1042
+ }
1043
+
1044
+ if (!target.data) target.data = {};
1045
+ target.data[propName] = typeStr;
1046
+
1047
+ // Remove :: and type tokens from stream
1048
+ let removeCount = 1 + typeTokens.length; // :: + type tokens
1049
+ tokens.splice(i, removeCount);
1050
+ return 0; // Re-examine current position
1051
+ }
1052
+
1053
+ // --- Handle ::= (type aliases) ---
1054
+ // Described in Phase 2 below
1055
+
1056
+ return 1;
1057
+ });
1058
+ }
845
1059
  ```
846
1060
 
847
- ## When to Use Runtime Enums
1061
+ #### 1.3 Type Expression Boundary Detection
1062
+
1063
+ The hardest part of type rewriting is determining where a type expression ends.
1064
+ The rewriter must **balance brackets** while scanning.
1065
+
1066
+ **Delimiters that end a type expression (at bracket depth 0):**
1067
+
1068
+ | Token | Why it ends the type |
1069
+ |-------|---------------------|
1070
+ | `=` `:=` `~=` `=!` `~>` | Assignment operator follows |
1071
+ | `->` | Code arrow — function body follows (not part of type) |
1072
+ | `TERMINATOR` | End of line |
1073
+ | `INDENT` / `OUTDENT` | Block boundary |
1074
+ | `)` `CALL_END` `PARAM_END` | End of parameter list |
1075
+ | `,` | Next parameter or next item |
1076
+
1077
+ **`=>` is NOT a delimiter** — it is the function type arrow. When `=>`
1078
+ appears at depth 0 inside a type expression, the type continues (to
1079
+ collect the return type). This is why types use `=>` exclusively: it
1080
+ disambiguates the function type arrow from the code arrow `->`, which
1081
+ always ends a type expression.
1082
+
1083
+ **Tokens that adjust bracket depth:**
1084
+
1085
+ | Open | Close | Context | Token tag |
1086
+ |------|-------|---------|-----------|
1087
+ | `<` | `>` | Generic type parameters | `COMPARE` (check `t[1]`) |
1088
+ | `(` | `)` | Function type parentheses | `(` / `)` or `CALL_START` / `CALL_END` |
1089
+ | `[` | `]` | Array type / index signatures | `[` / `]` or `INDEX_START` / `INDEX_END` |
1090
+
1091
+ **Worked examples:**
1092
+
1093
+ ```
1094
+ INPUT: count:: number = 0
1095
+ SCAN: :: → start collecting
1096
+ number → depth=0, type token
1097
+ = → depth=0, assignment delimiter, STOP
1098
+ RESULT: type = "number"
1099
+
1100
+ INPUT: items:: Map<string, number> = x
1101
+ SCAN: :: → start collecting
1102
+ Map → depth=0, type token
1103
+ < → depth=1, type token
1104
+ string → depth=1, type token
1105
+ , → depth=1 (inside <>), type token
1106
+ number → depth=1, type token
1107
+ > → depth=0, type token
1108
+ = → depth=0, assignment delimiter, STOP
1109
+ RESULT: type = "Map<string, number>"
1110
+
1111
+ INPUT: def f(a:: number, b:: string)
1112
+ SCAN: :: (after a) → start collecting
1113
+ number → depth=0, type token
1114
+ , → depth=0, parameter delimiter, STOP
1115
+ RESULT: type on a = "number"
1116
+ SCAN: :: (after b) → start collecting
1117
+ string → depth=0, type token
1118
+ ) → depth=0, close paren, STOP
1119
+ RESULT: type on b = "string"
1120
+
1121
+ INPUT: fn:: (a: number, b: string) => void = ...
1122
+ SCAN: :: → start collecting
1123
+ ( → depth=1, type token
1124
+ a → depth=1, type token
1125
+ : → depth=1, type token
1126
+ number → depth=1, type token
1127
+ , → depth=1 (inside parens), type token
1128
+ b → depth=1, type token
1129
+ : → depth=1, type token
1130
+ string → depth=1, type token
1131
+ ) → depth=0, type token
1132
+ => → depth=0, function type arrow, type token (CONTINUES)
1133
+ void → depth=0, type token
1134
+ = → depth=0, assignment delimiter, STOP
1135
+ RESULT: type = "(a: number, b: string) => void"
1136
+
1137
+ INPUT: (name:: string):: string -> "Hello!"
1138
+ SCAN: :: (after PARAM_END) → start collecting return type
1139
+ string → depth=0, type token
1140
+ -> → depth=0, code arrow delimiter, STOP
1141
+ RESULT: returnType = "string"
1142
+ -> stays in stream as the function body arrow
1143
+ ```
1144
+
1145
+ **Arrow disambiguation:** Types use `=>` exclusively, code uses `->`.
1146
+ This means the rewriter never has to guess what an arrow means during type
1147
+ collection — the token itself is the answer:
1148
+
1149
+ | Arrow | In type collection | Meaning |
1150
+ |-------|-------------------|---------|
1151
+ | `=>` | Continue collecting | Function type arrow (part of the type) |
1152
+ | `->` | Stop collecting | Code arrow (function body follows) |
1153
+
1154
+ **Other special cases:**
1155
+
1156
+ - **`?` / `??` / `!` suffixes**: These modify the type. When they appear
1157
+ unspaced after an identifier at depth 0, they are part of the type:
1158
+ `string?` → `"string?"`, `ID!` → `"ID!"`.
1159
+ - **Generic `<>` vs comparison**: Inside a type context (after `::`), always
1160
+ treat `<` as opening a generic bracket. The type context is unambiguous
1161
+ because we are already inside a `::` collection.
1162
+
1163
+ #### 1.4 Return Type Annotations
1164
+
1165
+ Return types appear after the parameter list close:
848
1166
 
849
- Use runtime enums only when you need:
1167
+ ```coffee
1168
+ def greet(name:: string):: string
1169
+ ```
850
1170
 
851
- - **Reverse mapping** Get name from value: `Status[0]` `"pending"`
852
- - **Iteration** Loop over all values at runtime
853
- - **Explicit numeric values** `HttpCode.ok === 200`
1171
+ After `rewriteTypes()` processes the `::` after `)`, the return type must be
1172
+ stored on the **function name IDENTIFIER**, not on `CALL_END`. This is because
1173
+ the grammar rule `["def", 2, 4, 6]` discards `CALL_END` (position 5) — any
1174
+ data stored there is lost when the parser builds the s-expression.
854
1175
 
855
- For all other cases, prefer zero-runtime union types.
1176
+ The `rewriteTypes()` code above handles this uniformly — in each case,
1177
+ find the token that **survives into the s-expression** and store there:
856
1178
 
857
- ---
1179
+ 1. **`def` with params** — preceding token is `CALL_END`/`)`: scan **backward**
1180
+ past the balanced parentheses to find the function name IDENTIFIER, store
1181
+ `.data.returnType` there.
1182
+ 2. **Arrow functions** — preceding token is `PARAM_END`: scan **forward** past
1183
+ the collected type tokens to find the `->` token, store `.data.returnType`
1184
+ there (`->` is the s-expression head: `["->", params, body]`).
1185
+ 3. **Parameterless `def`** — preceding token is an IDENTIFIER preceded by DEF:
1186
+ store `.data.returnType` directly on the name IDENTIFIER.
858
1187
 
859
- # 14. Boundary Validation
1188
+ **Property convention:**
1189
+ - `.data.type` — variable type, parameter type, property type
1190
+ - `.data.returnType` — function/method return type
1191
+ - `.data.typeParams` — generic type parameters (`<T, U>`)
860
1192
 
861
- ## Philosophy
1193
+ #### 1.5 Token Flow Examples
862
1194
 
863
- Types are compile-time contracts. For data entering your system (user input, API responses, file contents), **validate at boundaries**:
1195
+ Each example shows: Rip source tokens after rewrite s-expression outputs.
1196
+
1197
+ **Typed variable:**
864
1198
 
865
1199
  ```coffee
866
- # Type declares the shape
867
- User ::= type
868
- id: number
869
- name: string
870
- email: string
1200
+ count:: number = 0
1201
+ ```
871
1202
 
872
- # Validation enforces at runtime
873
- def parseUser(input:: unknown):: User
874
- UserSchema.parse(input) # Throws if invalid
1203
+ Tokens after `rewriteTypes()`:
1204
+ ```
1205
+ IDENTIFIER("count", {type: "number"}), =, NUMBER(0)
875
1206
  ```
876
1207
 
877
- ## Recommended Pattern
1208
+ S-expression (unchanged from untyped):
1209
+ ```
1210
+ ["=", count, 0] # count carries .data.type = "number"
1211
+ ```
878
1212
 
879
- Use a validation library (like Zod) at IO boundaries:
1213
+ .js output (type erased):
1214
+ ```js
1215
+ count = 0
1216
+ ```
1217
+
1218
+ .d.ts output:
1219
+ ```ts
1220
+ let count: number;
1221
+ ```
1222
+
1223
+ **Typed constant:**
880
1224
 
881
1225
  ```coffee
882
- import { z } from "zod"
1226
+ MAX:: number =! 100
1227
+ ```
883
1228
 
884
- # Define schema with runtime validation
885
- UserSchema = z.object
886
- id: z.number()
887
- name: z.string()
888
- email: z.string().email()
1229
+ Tokens: `IDENTIFIER("MAX", {type: "number"}), READONLY_ASSIGN, NUMBER(100)`
889
1230
 
890
- # Derive type from schema (single source of truth)
891
- User ::= z.infer<typeof UserSchema>
1231
+ S-expression: `["readonly", MAX, 100]` MAX carries `.data.type = "number"`
892
1232
 
893
- # Validate at API boundary
894
- def createUser(req:: Request):: User
895
- data = req.json!
896
- UserSchema.parse(data) # Validates and returns typed data
897
- ```
1233
+ .js: `const MAX = 100`
898
1234
 
899
- ## Trust Internally
1235
+ .d.ts: `declare const MAX: number;`
900
1236
 
901
- Once data passes boundary validation, trust the types internally:
1237
+ **Typed reactive state:**
902
1238
 
903
1239
  ```coffee
904
- def processUser(user:: User):: void
905
- # No need to re-validate — type guarantees shape
906
- sendWelcomeEmail(user.email)
907
- createProfile(user.id, user.name)
1240
+ count:: number := 0
1241
+ doubled:: number ~= count * 2
908
1242
  ```
909
1243
 
910
- ---
1244
+ S-expressions:
1245
+ ```
1246
+ ["state", count, 0] # count carries .data.type = "number"
1247
+ ["computed", doubled, ...] # doubled carries .data.type = "number"
1248
+ ```
911
1249
 
912
- # 15. Implementation Notes
1250
+ .d.ts:
1251
+ ```ts
1252
+ declare const count: Signal<number>;
1253
+ declare const doubled: Computed<number>;
1254
+ ```
1255
+
1256
+ **Typed function:**
913
1257
 
914
- ## Parser Changes
1258
+ ```coffee
1259
+ def getUser(id:: number):: User
1260
+ db.find!(id)
1261
+ ```
1262
+
1263
+ Tokens after rewrite:
1264
+ ```
1265
+ DEF, IDENTIFIER("getUser", {returnType: "User"}), CALL_START,
1266
+ IDENTIFIER("id", {type: "number"}),
1267
+ CALL_END, INDENT, ..., OUTDENT
1268
+ ```
915
1269
 
916
- The parser must recognize and preserve:
1270
+ S-expression: `["def", getUser, [id], body]`
1271
+ - `id` carries `.data.type = "number"`
1272
+ - `getUser` carries `.data.returnType = "User"`
917
1273
 
918
- 1. **`::` annotations** After identifiers, parameters, before return
919
- 2. **`::=` declarations** — Type alias definitions
920
- 3. **Type modifiers** — `?`, `??`, `!` suffixes
921
- 4. **`type` blocks** — Structural type definitions
922
- 5. **Generic syntax** — `<T>`, `<T extends U>`, etc.
923
- 6. **Block unions** — Leading `|` for union members
1274
+ .js: `async function getUser(id) { return db.find(id); }`
924
1275
 
925
- ## AST Representation
1276
+ .d.ts: `declare function getUser(id: number): User;`
926
1277
 
927
- Type information should be stored as AST metadata:
1278
+ **Typed arrow function:**
928
1279
 
929
1280
  ```coffee
930
- # Source
931
- count:: number = 0
1281
+ greet = (name:: string):: string -> "Hello, #{name}!"
932
1282
  ```
933
1283
 
934
- ```javascript
935
- // AST (conceptual)
936
- ["=", "count", 0, { type: "number" }]
1284
+ Tokens after rewrite:
1285
+ ```
1286
+ IDENTIFIER("greet"), =, PARAM_START,
1287
+ IDENTIFIER("name", {type: "string"}),
1288
+ PARAM_END, ->({returnType: "string"}), ...
937
1289
  ```
938
1290
 
939
- ## Code Generation
1291
+ **Class properties:**
940
1292
 
941
- Type annotations should:
1293
+ ```coffee
1294
+ class UserService
1295
+ db:: Database
1296
+ cache:: Map<string, User>
1297
+ ```
942
1298
 
943
- 1. **Strip from runtime** Never appear in `.js` output
944
- 2. **Preserve for `.d.ts`** — Emit valid TypeScript declarations
945
- 3. **Maintain source order** — Types in `.d.ts` match source file order
1299
+ Tokens: identifiers carry `.data.type`, grammar sees normal class body.
946
1300
 
947
- ## Scope and Validation
1301
+ .d.ts:
1302
+ ```ts
1303
+ declare class UserService {
1304
+ db: Database;
1305
+ cache: Map<string, User>;
1306
+ }
1307
+ ```
948
1308
 
949
- Rip does **not** need to:
1309
+ #### 1.6 Token-Level Type Emission
950
1310
 
951
- - Evaluate type expressions
952
- - Prove type soundness
953
- - Check type compatibility
954
- - Resolve type references
1311
+ After `rewriteTypes()` strips type annotations and stores metadata on
1312
+ tokens, the `emitTypes()` function scans the annotated token stream in a
1313
+ single forward pass and emits `.d.ts` declarations. This happens in
1314
+ `Compiler.compile()`, between tokenization and parsing — before the
1315
+ parser ever sees the tokens.
955
1316
 
956
- Rip only needs to:
1317
+ `emitTypes()` is a standalone function (not a class). It maintains a
1318
+ simple state machine:
957
1319
 
958
- - **Parse** type syntax correctly
959
- - **Preserve** type information in AST
960
- - **Emit** valid TypeScript type syntax
1320
+ - **Indent depth** tracks INDENT/OUTDENT to know nesting level
1321
+ - **Current class** when inside a CLASS body, emit class members
1322
+ - **Export flag** when EXPORT precedes a declaration, prepend `export`
961
1323
 
962
- All actual type checking is delegated to TypeScript when `types: "check"`.
1324
+ **Pattern matching** the function scans forward and recognizes:
963
1325
 
964
- ---
1326
+ | Token pattern | Emission |
1327
+ |---------------|----------|
1328
+ | `DEF IDENTIFIER(+returnType) CALL_START params CALL_END` | `function name(params): ReturnType;` |
1329
+ | `DEF IDENTIFIER(+returnType)` (no params) | `function name(): ReturnType;` |
1330
+ | `IDENTIFIER(+type) = ...` | `let name: Type;` |
1331
+ | `IDENTIFIER(+type) READONLY_ASSIGN ...` | `const name: Type;` |
1332
+ | `STATE IDENTIFIER(+type) ...` | `const name: Signal<Type>;` |
1333
+ | `COMPUTED IDENTIFIER(+type) ...` | `const name: Computed<Type>;` |
1334
+ | `CLASS IDENTIFIER INDENT ... OUTDENT` | `class Name { ... }` |
1335
+ | `EXPORT <declaration>` | Prepend `export` to the declaration |
1336
+ | `TYPE_DECL` marker | `type Name = TypeText;` or `interface Name { ... }` |
965
1337
 
966
- ## Summary
1338
+ **Rip-to-TypeScript conversions** — `emitTypes()` converts Rip type
1339
+ syntax into standard TypeScript:
967
1340
 
968
- Rip's optional type system provides:
1341
+ | Rip syntax | TypeScript equivalent |
1342
+ |-----------|---------------------|
1343
+ | `::` | `:` (annotation sigil to type separator) |
1344
+ | `T?` | `T \| undefined` |
1345
+ | `T??` | `T \| null \| undefined` |
1346
+ | `T!` | `NonNullable<T>` |
969
1347
 
970
- | Feature | Description |
971
- |---------|-------------|
972
- | **Type Annotations** | `::` for variables, parameters, returns |
973
- | **Type Aliases** | `::=` for named types |
974
- | **Optionality** | `T?` optional, `T??` nullable, `T!` non-null |
975
- | **Structural Types** | `type` blocks for object shapes |
976
- | **Union Types** | Inline or block form with `\|` |
977
- | **Function Types** | Arrow syntax, overloads supported |
978
- | **Generics** | Full generic support with constraints |
979
- | **Adoption Levels** | Project, file, and line granularity |
980
- | **Emission** | `.js` runtime + `.d.ts` declarations |
1348
+ Function type expressions use `=>` directly (same as TypeScript), so no
1349
+ arrow conversion is needed.
981
1350
 
982
- **The result:**
1351
+ The function returns a `.d.ts` string. Declarations without type
1352
+ annotations are skipped — only annotated code appears in the output.
983
1353
 
984
- - Type-driven development with TypeScript ecosystem compatibility
985
- - Optional adoption — add types where they help
986
- - Zero runtime cost — types are compile-time only
987
- - Minimal syntax — Rip stays Rip
1354
+ ### Phase 2: Type-Only Declarations
988
1355
 
989
- > **Static typing as a discipline, not a mandate.**
1356
+ These constructs exist only in the type system — they have no runtime
1357
+ representation (except `enum`). The rewriter handles type aliases and
1358
+ interfaces entirely (they never enter the grammar). Only `enum` needs a
1359
+ grammar rule because it emits runtime JavaScript.
990
1360
 
991
- ---
1361
+ #### 2.1 Keyword Migration
992
1362
 
993
- # 16. Quick Reference
1363
+ **Move `enum` and `interface` from `RESERVED` to `RIP_KEYWORDS`** in
1364
+ `src/lexer.js`. Currently (line 83):
994
1365
 
995
- ## Syntax Cheat Sheet
1366
+ ```js
1367
+ let RESERVED = new Set([
1368
+ 'case', 'function', 'var', 'void', 'with', 'const', 'let',
1369
+ 'enum', 'native', 'implements', 'interface', 'package',
1370
+ 'private', 'protected', 'public', 'static',
1371
+ ]);
1372
+ ```
996
1373
 
997
- ```coffee
998
- # ═══════════════════════════════════════════════════════════
999
- # TYPE ANNOTATIONS (::)
1000
- # ═══════════════════════════════════════════════════════════
1374
+ Remove `enum` and `interface` from `RESERVED`. Add them to `RIP_KEYWORDS`:
1001
1375
 
1002
- # Variables
1003
- count:: number = 0
1004
- name:: string = "Rip"
1005
- items:: string[] = []
1376
+ ```js
1377
+ let RIP_KEYWORDS = new Set([
1378
+ 'undefined', 'Infinity', 'NaN',
1379
+ 'then', 'unless', 'until', 'loop', 'of', 'by', 'when', 'def',
1380
+ 'component', 'render',
1381
+ 'enum', 'interface', // <-- ADD
1382
+ ]);
1383
+ ```
1006
1384
 
1007
- # Constants
1008
- MAX:: number =! 100
1385
+ No changes to `classifyKeyword()` are needed — the existing fallback
1386
+ `if (RIP_KEYWORDS.has(id)) return upper` (line 618) automatically tags
1387
+ `enum` as `ENUM` and `interface` as `INTERFACE`.
1009
1388
 
1010
- # Reactive state
1011
- count:: number := 0
1012
- doubled:: number ~= count * 2
1389
+ #### 2.2 Type Aliases (`::=`) — Unified typeText
1013
1390
 
1014
- # Function parameters and return
1015
- def greet(name:: string):: string
1016
- "Hello, #{name}!"
1391
+ The `::=` operator declares a named type. The `rewriteTypes()` pass handles
1392
+ it by collecting the right-hand side, converting it to a TypeScript-compatible
1393
+ type string, and packaging it into a single `TYPE_DECL` marker token. All
1394
+ three forms (simple alias, structural type, block union) produce the same
1395
+ metadata shape: `{ name, typeText }`. The `emitTypes()` function simply
1396
+ emits `type ${name} = ${typeText};` for all of them.
1017
1397
 
1018
- # ═══════════════════════════════════════════════════════════
1019
- # TYPE ALIASES (::=)
1020
- # ═══════════════════════════════════════════════════════════
1398
+ **Simple alias:**
1021
1399
 
1022
- # Simple alias
1400
+ ```coffee
1023
1401
  ID ::= number
1402
+ UserID ::= number | string
1403
+ ```
1024
1404
 
1025
- # Union (inline)
1026
- Status ::= "pending" | "active" | "done"
1405
+ When `rewriteTypes()` encounters `TYPE_ALIAS` (`::=`):
1027
1406
 
1028
- # Union (block - preferred)
1029
- HttpMethod ::=
1030
- | "GET"
1031
- | "POST"
1032
- | "PUT"
1033
- | "DELETE"
1407
+ 1. Record the preceding `IDENTIFIER` token as the type name
1408
+ 2. Collect all following tokens as the type body (same boundary rules as `::`)
1409
+ 3. Replace the entire sequence (`IDENTIFIER`, `TYPE_ALIAS`, type tokens) with
1410
+ a single `TYPE_DECL` marker token
1411
+
1412
+ ```js
1413
+ // Inside rewriteTypes(), handling TYPE_ALIAS:
1414
+ if (tag === 'TYPE_ALIAS') {
1415
+ let nameToken = tokens[i - 1];
1416
+ let name = nameToken[1];
1417
+
1418
+ // Collect type body tokens (same boundary logic as ::)
1419
+ let typeTokens = [];
1420
+ let j = i + 1;
1421
+ let depth = 0;
1422
+ // ... same collection loop as for TYPE_ANNOTATION ...
1423
+
1424
+ let typeStr = /* join collected tokens */;
1425
+
1426
+ // Replace name + ::= + type tokens with TYPE_DECL marker
1427
+ let declToken = gen('TYPE_DECL', name, nameToken);
1428
+ declToken.data = { name, typeText: typeStr };
1429
+ tokens.splice(i - 1, 2 + typeTokens.length, declToken);
1430
+ return 0;
1431
+ }
1432
+ ```
1433
+
1434
+ The `TYPE_DECL` marker is read by `emitTypes()` and then **removed from
1435
+ the token stream** before parsing. No grammar rule is needed.
1436
+
1437
+ .d.ts: `type ID = number;`
1438
+
1439
+ .js: *(nothing — type-only, erased)*
1440
+
1441
+ **Structural type:**
1034
1442
 
1035
- # Structural type
1443
+ ```coffee
1036
1444
  User ::= type
1037
1445
  id: number
1038
1446
  name: string
1039
1447
  email?: string
1448
+ ```
1040
1449
 
1041
- # Function type
1042
- Handler ::= (req:: Request) -> Response
1450
+ When `rewriteTypes()` sees `TYPE_ALIAS` followed by `IDENTIFIER("type")` and
1451
+ then `INDENT`, it collects the block body and converts it to a TypeScript
1452
+ object type string:
1043
1453
 
1044
- # Generic type
1045
- Container<T> ::= type
1046
- value: T
1454
+ 1. Consume the `INDENT`
1455
+ 2. Collect property declarations line by line until `OUTDENT`
1456
+ 3. Convert to TypeScript object syntax: `"{ id: number; name: string; email?: string; }"`
1457
+ 4. Store as a single typeText string — no structured property metadata needed:
1458
+ ```js
1459
+ declToken.data = {
1460
+ name: "User",
1461
+ typeText: "{ id: number; name: string; email?: string; }"
1462
+ };
1463
+ ```
1464
+ 5. Replace entire sequence with single `TYPE_DECL` marker
1465
+
1466
+ The rewriter does the Rip-to-TypeScript formatting (indented properties to
1467
+ `{ prop: type; ... }`). The emitter just writes `type ${name} = ${typeText};`
1468
+ without needing to understand the internal structure.
1047
1469
 
1048
- # ═══════════════════════════════════════════════════════════
1049
- # OPTIONALITY MODIFIERS
1050
- # ═══════════════════════════════════════════════════════════
1470
+ .d.ts:
1471
+ ```ts
1472
+ type User = {
1473
+ id: number;
1474
+ name: string;
1475
+ email?: string;
1476
+ };
1477
+ ```
1051
1478
 
1052
- email:: string? # T | undefined
1053
- middle:: string?? # T | null | undefined
1054
- id:: ID! # NonNullable<T>
1479
+ **Block union:**
1055
1480
 
1056
- # ═══════════════════════════════════════════════════════════
1057
- # GENERICS
1058
- # ═══════════════════════════════════════════════════════════
1481
+ ```coffee
1482
+ HttpMethod ::=
1483
+ | "GET"
1484
+ | "POST"
1485
+ | "PUT"
1486
+ | "DELETE"
1487
+ ```
1059
1488
 
1060
- def identity<T>(x:: T):: T
1061
- x
1489
+ When `rewriteTypes()` sees `TYPE_ALIAS` followed by `TERMINATOR` or `INDENT`
1490
+ with leading `|` tokens, it collects union members and joins them:
1062
1491
 
1063
- def map<T, U>(arr:: T[], fn:: (x:: T) -> U):: U[]
1064
- arr.map(fn)
1492
+ ```js
1493
+ declToken.data = {
1494
+ name: "HttpMethod",
1495
+ typeText: '"GET" | "POST" | "PUT" | "DELETE"'
1496
+ };
1497
+ ```
1065
1498
 
1066
- # With constraints
1067
- def merge<T extends object>(a:: T, b:: T):: T
1068
- {...a, ...b}
1499
+ .d.ts: `type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";`
1069
1500
 
1070
- # ═══════════════════════════════════════════════════════════
1071
- # INTERFACES
1072
- # ═══════════════════════════════════════════════════════════
1501
+ #### 2.3 Interfaces — Rewriter-Based
1073
1502
 
1503
+ ```coffee
1074
1504
  interface Animal
1075
1505
  name: string
1076
1506
 
1077
1507
  interface Dog extends Animal
1078
1508
  breed: string
1509
+ bark: () => void
1510
+ ```
1511
+
1512
+ Interfaces are type-only (no .js output), so they are handled entirely by
1513
+ the rewriter — no grammar rule needed. When `rewriteTypes()` encounters an
1514
+ `INTERFACE` token:
1515
+
1516
+ 1. Record the interface name
1517
+ 2. If followed by `EXTENDS`, record the parent name
1518
+ 3. Collect the body block (between `INDENT` and `OUTDENT`)
1519
+ 4. Convert to TypeScript interface body string
1520
+ 5. Replace the entire sequence with a `TYPE_DECL` marker:
1521
+ ```js
1522
+ declToken.data = {
1523
+ name: "Dog",
1524
+ kind: "interface",
1525
+ extends: "Animal",
1526
+ typeText: "{ breed: string; bark: () => void; }"
1527
+ };
1528
+ ```
1529
+
1530
+ The `emitTypes()` function recognizes kind `"interface"` and emits:
1531
+
1532
+ ```ts
1533
+ interface Dog extends Animal {
1534
+ breed: string;
1535
+ bark: () => void;
1536
+ }
1537
+ ```
1538
+
1539
+ The `TYPE_DECL` marker is removed before parsing. The parser and compiler
1540
+ never see interfaces.
1079
1541
 
1080
- # ═══════════════════════════════════════════════════════════
1081
- # ENUMS (use sparingly)
1082
- # ═══════════════════════════════════════════════════════════
1542
+ .js: *(nothing — erased)*
1083
1543
 
1084
- enum Status
1085
- pending
1086
- active
1087
- done
1544
+ #### 2.4 Enums
1088
1545
 
1546
+ ```coffee
1089
1547
  enum HttpCode
1090
1548
  ok = 200
1549
+ created = 201
1091
1550
  notFound = 404
1551
+ serverError = 500
1092
1552
  ```
1093
1553
 
1094
- ## Comparison: Rip vs TypeScript vs CoffeeScript
1554
+ Enums are the **one** type construct that emits both .js and .d.ts. They
1555
+ are the only construct that needs a grammar rule and a compiler generator.
1095
1556
 
1096
- | Feature | Rip | TypeScript | CoffeeScript |
1097
- |---------|-----|------------|--------------|
1098
- | Type annotations | `x:: T` | `x: T` | N/A |
1099
- | Type aliases | `T ::= ...` | `type T = ...` | N/A |
1100
- | Optional type | `T?` | `T \| undefined` | N/A |
1101
- | Nullable | `T??` | `T \| null \| undefined` | N/A |
1102
- | Non-nullable | `T!` | `NonNullable<T>` | N/A |
1103
- | Structural types | `type` block | `{ ... }` | N/A |
1104
- | Block unions | `\| "a" \| "b"` | Same | N/A |
1105
- | Types required | No | Configurable | N/A |
1106
- | Runtime cost | Zero | Zero | N/A |
1557
+ **Grammar rule:**
1107
1558
 
1108
- ## Project Configuration
1559
+ ```
1560
+ Enum: [
1561
+ o 'ENUM Identifier Block', '["enum", 2, 3]'
1562
+ ]
1563
+ ```
1109
1564
 
1110
- ```json
1111
- // package.json
1112
- {
1113
- "rip": {
1114
- "types": "emit" // "off" | "emit" | "check"
1565
+ Add `Enum` to the `Expression` list.
1566
+
1567
+ S-expression: `["enum", "HttpCode", body]`
1568
+
1569
+ .js (runtime reverse-mapping object, via `generateEnum()`):
1570
+ ```js
1571
+ const HttpCode = {
1572
+ ok: 200, created: 201, notFound: 404, serverError: 500,
1573
+ 200: "ok", 201: "created", 404: "notFound", 500: "serverError"
1574
+ };
1575
+ ```
1576
+
1577
+ .d.ts (emitted by `emitTypes()` from the token stream):
1578
+ ```ts
1579
+ enum HttpCode {
1580
+ ok = 200,
1581
+ created = 201,
1582
+ notFound = 404,
1583
+ serverError = 500
1584
+ }
1585
+ ```
1586
+
1587
+ Note: `emitTypes()` reads enum info from the token stream before parsing.
1588
+ The enum tokens remain in the stream for the parser (unlike type aliases
1589
+ and interfaces, which are removed).
1590
+
1591
+ #### 2.5 Grammar Changes Summary
1592
+
1593
+ In `src/grammar/grammar.rip`, add `Enum` to the `Expression` list:
1594
+
1595
+ ```
1596
+ Expression: [
1597
+ o 'Value'
1598
+ o 'Code'
1599
+ o 'Operation'
1600
+ o 'Assign'
1601
+ o 'ReactiveAssign'
1602
+ o 'ComputedAssign'
1603
+ o 'ReadonlyAssign'
1604
+ o 'ReactAssign'
1605
+ o 'Enum' # <-- ADD (only type construct needing grammar)
1606
+ o 'If'
1607
+ ...
1608
+ ]
1609
+ ```
1610
+
1611
+ And add the single new rule:
1612
+
1613
+ ```
1614
+ Enum: [
1615
+ o 'ENUM Identifier Block', '["enum", 2, 3]'
1616
+ ]
1617
+ ```
1618
+
1619
+ Also add to `Export`:
1620
+
1621
+ ```
1622
+ o 'EXPORT Enum' , '["export", 2]'
1623
+ ```
1624
+
1625
+ No `TypeDecl` or `Interface` grammar rules — those are handled entirely by
1626
+ the rewriter and removed before parsing.
1627
+
1628
+ #### 2.6 Generic Type Parameters
1629
+
1630
+ ```coffee
1631
+ def identity<T>(value:: T):: T
1632
+ value
1633
+
1634
+ def map<T, U>(items:: T[], fn:: (item:: T) => U):: U[]
1635
+ items.map(fn)
1636
+ ```
1637
+
1638
+ Generic parameters on functions require special handling because `<` is
1639
+ normally a comparison operator.
1640
+
1641
+ **Detection via `.spaced` property:**
1642
+
1643
+ The lexer tracks whitespace before every token via the `.spaced` property.
1644
+ In `def identity<T>`, the `<` token is unspaced from the identifier — this
1645
+ is the key signal. In a comparison `x < y`, the `<` is spaced.
1646
+
1647
+ When `rewriteTypes()` sees `DEF IDENTIFIER` followed by an unspaced `<`:
1648
+
1649
+ 1. Treat it as a generic parameter list
1650
+ 2. Collect balanced `<...>` tokens as a string (e.g., `"<T>"`, `"<T, U>"`,
1651
+ `"<T extends Ordered>"`)
1652
+ 3. Store on the function name token as `.data.typeParams`
1653
+ 4. Remove the `<...>` tokens from the stream
1654
+
1655
+ ```js
1656
+ // Generic detection using .spaced:
1657
+ if (tag === 'IDENTIFIER' && i >= 1 && tokens[i - 1]?.[0] === 'DEF') {
1658
+ let next = tokens[i + 1];
1659
+ if (next && next[0] === 'COMPARE' && next[1] === '<' && !next.spaced) {
1660
+ let genTokens = collectBalancedAngleBrackets(tokens, i + 1);
1661
+ if (genTokens) {
1662
+ if (!token.data) token.data = {};
1663
+ token.data.typeParams = joinTokens(genTokens);
1664
+ tokens.splice(i + 1, genTokens.length);
1665
+ }
1115
1666
  }
1116
1667
  }
1117
1668
  ```
1118
1669
 
1119
- ## File Directives
1670
+ Generic parameters on type aliases work the same way:
1120
1671
 
1121
1672
  ```coffee
1122
- # @types off — Ignore types in this file
1123
- # @types emit — Parse and emit .d.ts
1124
- # @types check — Full TypeScript checking
1673
+ Container<T> ::= type
1674
+ value: T
1675
+ ```
1676
+
1677
+ The `rewriteTypes()` pass detects `IDENTIFIER` + unspaced `<...>` +
1678
+ `TYPE_ALIAS` and collects the generic params before processing the `::=`.
1679
+
1680
+ .d.ts:
1681
+ ```ts
1682
+ function identity<T>(value: T): T;
1683
+ function map<T, U>(items: T[], fn: (item: T) => U): U[];
1684
+ type Container<T> = { value: T; };
1685
+ ```
1686
+
1687
+ ### Phase 3: Dual Emission (.js and .d.ts)
1688
+
1689
+ All type-related logic lives in `src/types.js`. The `.d.ts` is emitted from
1690
+ the annotated token stream (before parsing). The `.js` is generated by the
1691
+ existing CodeGenerator (after parsing), with only `generateEnum()` added.
1692
+
1693
+ #### 3.1 `generateEnum()` — The One Compiler Addition
1694
+
1695
+ Enums are the only type construct that produces runtime JavaScript. The
1696
+ `generateEnum()` function is exported from `types.js` and wired onto the
1697
+ CodeGenerator prototype:
1698
+
1699
+ ```js
1700
+ // In src/types.js
1701
+ export function generateEnum(head, rest, context) {
1702
+ let [name, body] = rest;
1703
+ // Parse body into key-value pairs
1704
+ let pairs = this.parseEnumBody(body);
1705
+ let forward = pairs.map(([k, v]) => `${k}: ${v}`).join(', ');
1706
+ let reverse = pairs.map(([k, v]) => `${v}: "${k}"`).join(', ');
1707
+ return `const ${name} = {${forward}, ${reverse}}`;
1708
+ }
1709
+ ```
1710
+
1711
+ **Wiring in `src/compiler.js`:**
1712
+
1713
+ ```js
1714
+ import { emitTypes, generateEnum } from './types.js';
1715
+ CodeGenerator.prototype.generateEnum = generateEnum;
1716
+ CodeGenerator.GENERATORS['enum'] = 'generateEnum';
1125
1717
  ```
1126
1718
 
1719
+ No `generateTypeAlias()` or `generateInterface()` methods are needed —
1720
+ type aliases and interfaces are removed from the token stream by the
1721
+ rewriter before the parser ever sees them.
1722
+
1723
+ #### 3.2 `emitTypes()` — Token-Level .d.ts Emission
1724
+
1725
+ The `emitTypes()` function replaces the `DtsGenerator` class. Instead of
1726
+ walking the s-expression tree, it scans the annotated token stream in a
1727
+ single forward pass. This is simpler because:
1728
+
1729
+ - It doesn't duplicate the CodeGenerator's dispatch logic
1730
+ - It only handles declarations (not all AST node types)
1731
+ - It reads metadata that `rewriteTypes()` already placed on tokens
1732
+
1733
+ See §1.6 for the pattern-matching table and state machine description.
1734
+
1735
+ #### 3.3 Compiler Wiring
1736
+
1737
+ In `src/compiler.js`, the compilation pipeline becomes:
1738
+
1739
+ ```js
1740
+ compile(source) {
1741
+ // Step 1: Tokenize (includes rewriteTypes() via installTypeSupport)
1742
+ let lexer = new Lexer();
1743
+ let tokens = lexer.tokenize(source);
1744
+
1745
+ // Step 2: Emit .d.ts from annotated tokens (before parsing)
1746
+ let dts = null;
1747
+ if (this.options.types === 'emit' || this.options.types === 'check') {
1748
+ dts = emitTypes(tokens);
1749
+ // Remove TYPE_DECL markers so the parser doesn't see them
1750
+ tokens = tokens.filter(t => t[0] !== 'TYPE_DECL');
1751
+ }
1752
+
1753
+ // Step 3: Parse (grammar only sees Enum as a new construct)
1754
+ // ... existing parser setup ...
1755
+ let sexpr = parser.parse(source);
1756
+
1757
+ // Step 4: Generate .js (CodeGenerator only needs generateEnum)
1758
+ let generator = new CodeGenerator({ ... });
1759
+ let code = generator.compile(sexpr);
1760
+
1761
+ return { tokens, sexpr, code, dts, data: dataSection, reactiveVars: generator.reactiveVars };
1762
+ }
1763
+ ```
1764
+
1765
+ The key insight: `.d.ts` emission happens **between tokenization and
1766
+ parsing**. After `emitTypes()` reads the annotated tokens, `TYPE_DECL`
1767
+ markers are filtered out. The parser receives a clean, type-free token
1768
+ stream — identical to what it would see from untyped Rip code, plus
1769
+ `ENUM` tokens.
1770
+
1771
+ #### 3.4 `const` Emission Rule
1772
+
1773
+ Type annotations never affect `let` vs `const` in `.js` output:
1774
+
1775
+ | Rip operator | .js keyword | .d.ts keyword |
1776
+ |-------------|-------------|---------------|
1777
+ | `=` | `let` (hoisted by `programVars`) | `let` |
1778
+ | `=!` | `const` | `const` |
1779
+ | `:=` | `const` (reactive signal) | `const` (Signal) |
1780
+ | `~=` | `const` (computed signal) | `const` (Computed) |
1781
+ | `~>` | `const` (effect) | `const` |
1782
+
1783
+ ### Edge Cases
1784
+
1785
+ #### Optionality Suffixes in Types
1786
+
1787
+ The `?`, `??`, and `!` suffixes appear unspaced after the type name:
1788
+
1789
+ ```coffee
1790
+ email:: string? # → type = "string?"
1791
+ middle:: string?? # → type = "string??"
1792
+ id:: ID! # → type = "ID!"
1793
+ ```
1794
+
1795
+ In `rewriteTypes()`, when collecting type tokens, if the next token is `?`,
1796
+ `??`, or `!` and is **not spaced** from the previous token, include it as
1797
+ part of the type string.
1798
+
1799
+ The `emitTypes()` function expands these suffixes into standard TypeScript:
1800
+
1801
+ | Rip suffix | TypeScript equivalent |
1802
+ |-----------|---------------------|
1803
+ | `T?` | `T \| undefined` |
1804
+ | `T??` | `T \| null \| undefined` |
1805
+ | `T!` | `NonNullable<T>` |
1806
+
1807
+ See §1.6 for the full Rip-to-TypeScript conversion table (including `::` → `:`).
1808
+
1809
+ #### File-Level Type Directives
1810
+
1811
+ ```coffee
1812
+ # @types off — ignore types in this file
1813
+ # @types emit — emit .d.ts
1814
+ # @types check — emit .d.ts + enable tsc validation
1815
+ ```
1816
+
1817
+ These are comments. The lexer's `commentToken()` method can detect this
1818
+ pattern and set a flag. Alternatively, the `Compiler` can scan for the
1819
+ directive before tokenizing.
1820
+
1821
+ #### Export of Type-Only Declarations
1822
+
1823
+ ```coffee
1824
+ export User ::= type
1825
+ id: number
1826
+ name: string
1827
+
1828
+ export def getUser(id:: number):: User?
1829
+ db.find(id)
1830
+ ```
1831
+
1832
+ The rewriter detects `EXPORT` before `::=` sequences and marks the
1833
+ `TYPE_DECL` marker accordingly. The `emitTypes()` function prepends
1834
+ `export` to the declaration. No grammar rule is involved for type-only
1835
+ exports — the `TYPE_DECL` marker is removed before parsing.
1836
+
1837
+ For exported functions, the existing `Export` grammar rules handle
1838
+ `EXPORT Expression` as before.
1839
+
1840
+ .js (type alias erased, function exported):
1841
+ ```js
1842
+ export function getUser(id) {
1843
+ return db.find(id);
1844
+ }
1845
+ ```
1846
+
1847
+ .d.ts (both exported):
1848
+ ```ts
1849
+ export type User = { id: number; name: string; };
1850
+ export function getUser(id: number): User | undefined;
1851
+ ```
1852
+
1853
+ ### Test Matrix
1854
+
1855
+ Each row is a test case. Verify both .js and .d.ts output.
1856
+
1857
+ | # | Rip Input | .js Output | .d.ts Output |
1858
+ |---|-----------|-----------|-------------|
1859
+ | 1 | `count:: number = 0` | `count = 0` | `let count: number;` |
1860
+ | 2 | `MAX:: number =! 100` | `const MAX = 100` | `declare const MAX: number;` |
1861
+ | 3 | `count:: number := 0` | `const count = __state(0)` | `declare const count: Signal<number>;` |
1862
+ | 4 | `doubled:: number ~= x * 2` | `const doubled = __computed(...)` | `declare const doubled: Computed<number>;` |
1863
+ | 5 | `def f(a:: number):: string` | `function f(a) { ... }` | `declare function f(a: number): string;` |
1864
+ | 6 | `(x:: number):: number -> x + 1` | `(x) => x + 1` | `(x: number) => number` |
1865
+ | 7 | `ID ::= number` | *(empty)* | `type ID = number;` |
1866
+ | 8 | `User ::= type` (+ block) | *(empty)* | `type User = { id: number; ... };` |
1867
+ | 9 | `Status ::= \| "a" \| "b"` | *(empty)* | `type Status = "a" \| "b";` |
1868
+ | 10 | `interface Foo` (+ block) | *(empty)* | `interface Foo { ... }` |
1869
+ | 11 | `enum Code` (+ block) | `const Code = { ... }` | `enum Code { ... }` |
1870
+ | 12 | `items:: Map<string, number> = x` | `items = x` | `let items: Map<string, number>;` |
1871
+ | 13 | `email:: string?` | — | `let email: string \| undefined;` |
1872
+ | 14 | `id:: ID!` | — | `let id: NonNullable<ID>;` |
1873
+ | 15 | `def identity<T>(v:: T):: T` | `function identity(v) { ... }` | `declare function identity<T>(v: T): T;` |
1874
+ | 16 | `export User ::= type` (+ block) | *(empty)* | `export type User = { ... };` |
1875
+ | 17 | `export def f(x:: number):: number` | `export function f(x) { ... }` | `export function f(x: number): number;` |
1876
+
1127
1877
  ---
1128
1878
 
1129
1879
  **See Also:**