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/README.md +23 -1
- package/bin/rip +24 -2
- package/docs/RIP-INTERNALS.md +18 -8
- package/docs/RIP-LANG.md +41 -6
- package/docs/RIP-REACTIVITY.md +21 -0
- package/docs/RIP-TYPES.md +1345 -595
- package/docs/dist/rip.browser.js +902 -303
- package/docs/dist/rip.browser.min.js +233 -231
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/package.json +3 -3
- package/src/compiler.js +34 -4
- package/src/grammar/grammar.rip +10 -0
- package/src/lexer.js +14 -2
- package/src/parser.js +216 -215
- package/src/types.js +718 -0
package/docs/RIP-TYPES.md
CHANGED
|
@@ -1,46 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Rip Types
|
|
2
2
|
|
|
3
|
-
> **
|
|
3
|
+
> **Types describe what you mean. Code does what you say.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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.
|
|
28
|
+
Both compile to identical JavaScript.
|
|
56
29
|
|
|
57
|
-
|
|
30
|
+
---
|
|
58
31
|
|
|
59
|
-
|
|
32
|
+
## Table of Contents
|
|
60
33
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
This is the complete Rip Types sigil vocabulary.
|
|
71
69
|
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
154
|
-
doubled:: number ~= count * 2
|
|
160
|
+
count:: number := 0
|
|
161
|
+
doubled:: number ~= count * 2
|
|
155
162
|
```
|
|
156
163
|
|
|
157
|
-
|
|
164
|
+
**Emits:**
|
|
158
165
|
|
|
159
|
-
|
|
166
|
+
```ts
|
|
167
|
+
const count = __state<number>(0); // := emits const
|
|
168
|
+
const doubled = __computed<number>(() => count * 2); // ~= emits const
|
|
169
|
+
```
|
|
160
170
|
|
|
161
|
-
|
|
171
|
+
---
|
|
162
172
|
|
|
163
|
-
|
|
173
|
+
## Type Aliases (`::=`)
|
|
164
174
|
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Structural Types
|
|
203
|
+
|
|
204
|
+
Define object shapes using the `type` keyword followed by a block:
|
|
182
205
|
|
|
183
206
|
```coffee
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
283
|
+
## Optionality Modifiers
|
|
200
284
|
|
|
201
285
|
Lightweight suffix operators that map directly to TypeScript unions.
|
|
202
286
|
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
In object types, `?` after the property name makes it optional:
|
|
348
|
+
**Emits:**
|
|
267
349
|
|
|
268
|
-
```
|
|
269
|
-
User
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
388
|
+
## Union Types
|
|
373
389
|
|
|
374
|
-
|
|
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
|
-
|
|
397
|
+
### Block Form (Preferred)
|
|
383
398
|
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
## Arrow Function Types
|
|
434
|
+
## Function Types
|
|
446
435
|
|
|
447
436
|
```coffee
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
528
|
+
---
|
|
544
529
|
|
|
545
|
-
|
|
546
|
-
def identity<T>(value:: T):: T
|
|
547
|
-
value
|
|
530
|
+
## Interfaces
|
|
548
531
|
|
|
549
|
-
|
|
550
|
-
items.map(fn)
|
|
532
|
+
For TypeScript compatibility, Rip supports the `interface` keyword:
|
|
551
533
|
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
543
|
+
**Emits:**
|
|
557
544
|
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
|
|
545
|
+
```ts
|
|
546
|
+
interface Animal {
|
|
547
|
+
name: string;
|
|
548
|
+
}
|
|
561
549
|
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
612
|
+
## Type Inference
|
|
573
613
|
|
|
574
|
-
|
|
614
|
+
Types should be explicit at boundaries, optional elsewhere:
|
|
575
615
|
|
|
576
616
|
```coffee
|
|
577
|
-
#
|
|
578
|
-
def processUser(id:: number, options::
|
|
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
|
-
|
|
593
|
-
|
|
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
|
|
630
|
+
count = 0 # Inferred as number
|
|
597
631
|
```
|
|
598
632
|
|
|
599
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
Types are optional at three levels: project, file, and line.
|
|
642
|
+
## Adoption Model
|
|
611
643
|
|
|
612
|
-
|
|
644
|
+
Types are optional at every level:
|
|
613
645
|
|
|
614
|
-
|
|
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"` |
|
|
627
|
-
| `"emit"` |
|
|
628
|
-
| `"check"` |
|
|
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
|
-
|
|
639
|
-
# Types parsed and emitted for this file
|
|
662
|
+
### File Level
|
|
640
663
|
|
|
641
|
-
|
|
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
|
-
#
|
|
651
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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.
|
|
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
|
-
|
|
709
|
+
**Generates `user.d.ts`** — types preserved:
|
|
708
710
|
|
|
709
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
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
|
-
|
|
748
|
+
Rip Types is primarily **editor-driven**. The intended loop:
|
|
720
749
|
|
|
721
|
-
1.
|
|
722
|
-
2.
|
|
723
|
-
3.
|
|
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
|
-
|
|
755
|
+
High-quality `.d.ts` output is a first-class goal.
|
|
726
756
|
|
|
727
757
|
---
|
|
728
758
|
|
|
729
|
-
|
|
759
|
+
## What Rip Intentionally Does Not Do
|
|
730
760
|
|
|
731
|
-
|
|
761
|
+
Rip does not:
|
|
732
762
|
|
|
733
|
-
|
|
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
|
-
|
|
736
|
-
|
|
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
|
-
|
|
772
|
+
Rip only needs to:
|
|
743
773
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
778
|
+
---
|
|
753
779
|
|
|
754
|
-
|
|
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
|
-
|
|
782
|
+
Rip Types provides:
|
|
762
783
|
|
|
763
|
-
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
name: string
|
|
790
|
+
Rip remains a **JavaScript language**, with types as a **design language**
|
|
791
|
+
layered on top.
|
|
768
792
|
|
|
769
|
-
|
|
770
|
-
breed: string
|
|
771
|
-
bark: () -> void
|
|
772
|
-
```
|
|
793
|
+
> **Static typing as a discipline, not a mandate.**
|
|
773
794
|
|
|
774
795
|
---
|
|
775
796
|
|
|
776
|
-
|
|
797
|
+
## Implementation Plan
|
|
777
798
|
|
|
778
|
-
|
|
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
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
**
|
|
848
|
+
**The parallel for types:**
|
|
798
849
|
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
|
|
869
|
+
### Phase 1: Type Annotations (Metadata on Existing Tokens)
|
|
805
870
|
|
|
806
|
-
|
|
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
|
-
|
|
809
|
-
enum Status
|
|
810
|
-
pending
|
|
811
|
-
active
|
|
812
|
-
completed
|
|
813
|
-
cancelled
|
|
877
|
+
#### 1.1 Lexer: Token Detection
|
|
814
878
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1167
|
+
```coffee
|
|
1168
|
+
def greet(name:: string):: string
|
|
1169
|
+
```
|
|
850
1170
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1193
|
+
#### 1.5 Token Flow Examples
|
|
862
1194
|
|
|
863
|
-
|
|
1195
|
+
Each example shows: Rip source → tokens after rewrite → s-expression → outputs.
|
|
1196
|
+
|
|
1197
|
+
**Typed variable:**
|
|
864
1198
|
|
|
865
1199
|
```coffee
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
id: number
|
|
869
|
-
name: string
|
|
870
|
-
email: string
|
|
1200
|
+
count:: number = 0
|
|
1201
|
+
```
|
|
871
1202
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1203
|
+
Tokens after `rewriteTypes()`:
|
|
1204
|
+
```
|
|
1205
|
+
IDENTIFIER("count", {type: "number"}), =, NUMBER(0)
|
|
875
1206
|
```
|
|
876
1207
|
|
|
877
|
-
|
|
1208
|
+
S-expression (unchanged from untyped):
|
|
1209
|
+
```
|
|
1210
|
+
["=", count, 0] # count carries .data.type = "number"
|
|
1211
|
+
```
|
|
878
1212
|
|
|
879
|
-
|
|
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
|
-
|
|
1226
|
+
MAX:: number =! 100
|
|
1227
|
+
```
|
|
883
1228
|
|
|
884
|
-
|
|
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
|
-
|
|
891
|
-
User ::= z.infer<typeof UserSchema>
|
|
1231
|
+
S-expression: `["readonly", MAX, 100]` — MAX carries `.data.type = "number"`
|
|
892
1232
|
|
|
893
|
-
|
|
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
|
-
|
|
1235
|
+
.d.ts: `declare const MAX: number;`
|
|
900
1236
|
|
|
901
|
-
|
|
1237
|
+
**Typed reactive state:**
|
|
902
1238
|
|
|
903
1239
|
```coffee
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1270
|
+
S-expression: `["def", getUser, [id], body]`
|
|
1271
|
+
- `id` carries `.data.type = "number"`
|
|
1272
|
+
- `getUser` carries `.data.returnType = "User"`
|
|
917
1273
|
|
|
918
|
-
|
|
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
|
-
|
|
1276
|
+
.d.ts: `declare function getUser(id: number): User;`
|
|
926
1277
|
|
|
927
|
-
|
|
1278
|
+
**Typed arrow function:**
|
|
928
1279
|
|
|
929
1280
|
```coffee
|
|
930
|
-
#
|
|
931
|
-
count:: number = 0
|
|
1281
|
+
greet = (name:: string):: string -> "Hello, #{name}!"
|
|
932
1282
|
```
|
|
933
1283
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
-
|
|
1291
|
+
**Class properties:**
|
|
940
1292
|
|
|
941
|
-
|
|
1293
|
+
```coffee
|
|
1294
|
+
class UserService
|
|
1295
|
+
db:: Database
|
|
1296
|
+
cache:: Map<string, User>
|
|
1297
|
+
```
|
|
942
1298
|
|
|
943
|
-
|
|
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
|
-
|
|
1301
|
+
.d.ts:
|
|
1302
|
+
```ts
|
|
1303
|
+
declare class UserService {
|
|
1304
|
+
db: Database;
|
|
1305
|
+
cache: Map<string, User>;
|
|
1306
|
+
}
|
|
1307
|
+
```
|
|
948
1308
|
|
|
949
|
-
|
|
1309
|
+
#### 1.6 Token-Level Type Emission
|
|
950
1310
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
1317
|
+
`emitTypes()` is a standalone function (not a class). It maintains a
|
|
1318
|
+
simple state machine:
|
|
957
1319
|
|
|
958
|
-
- **
|
|
959
|
-
- **
|
|
960
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
1338
|
+
**Rip-to-TypeScript conversions** — `emitTypes()` converts Rip type
|
|
1339
|
+
syntax into standard TypeScript:
|
|
967
1340
|
|
|
968
|
-
Rip
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1363
|
+
**Move `enum` and `interface` from `RESERVED` to `RIP_KEYWORDS`** in
|
|
1364
|
+
`src/lexer.js`. Currently (line 83):
|
|
994
1365
|
|
|
995
|
-
|
|
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
|
-
|
|
998
|
-
# ═══════════════════════════════════════════════════════════
|
|
999
|
-
# TYPE ANNOTATIONS (::)
|
|
1000
|
-
# ═══════════════════════════════════════════════════════════
|
|
1374
|
+
Remove `enum` and `interface` from `RESERVED`. Add them to `RIP_KEYWORDS`:
|
|
1001
1375
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
1011
|
-
count:: number := 0
|
|
1012
|
-
doubled:: number ~= count * 2
|
|
1389
|
+
#### 2.2 Type Aliases (`::=`) — Unified typeText
|
|
1013
1390
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1400
|
+
```coffee
|
|
1023
1401
|
ID ::= number
|
|
1402
|
+
UserID ::= number | string
|
|
1403
|
+
```
|
|
1024
1404
|
|
|
1025
|
-
|
|
1026
|
-
Status ::= "pending" | "active" | "done"
|
|
1405
|
+
When `rewriteTypes()` encounters `TYPE_ALIAS` (`::=`):
|
|
1027
1406
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1443
|
+
```coffee
|
|
1036
1444
|
User ::= type
|
|
1037
1445
|
id: number
|
|
1038
1446
|
name: string
|
|
1039
1447
|
email?: string
|
|
1448
|
+
```
|
|
1040
1449
|
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
1470
|
+
.d.ts:
|
|
1471
|
+
```ts
|
|
1472
|
+
type User = {
|
|
1473
|
+
id: number;
|
|
1474
|
+
name: string;
|
|
1475
|
+
email?: string;
|
|
1476
|
+
};
|
|
1477
|
+
```
|
|
1051
1478
|
|
|
1052
|
-
|
|
1053
|
-
middle:: string?? # T | null | undefined
|
|
1054
|
-
id:: ID! # NonNullable<T>
|
|
1479
|
+
**Block union:**
|
|
1055
1480
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1481
|
+
```coffee
|
|
1482
|
+
HttpMethod ::=
|
|
1483
|
+
| "GET"
|
|
1484
|
+
| "POST"
|
|
1485
|
+
| "PUT"
|
|
1486
|
+
| "DELETE"
|
|
1487
|
+
```
|
|
1059
1488
|
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
-
|
|
1064
|
-
|
|
1492
|
+
```js
|
|
1493
|
+
declToken.data = {
|
|
1494
|
+
name: "HttpMethod",
|
|
1495
|
+
typeText: '"GET" | "POST" | "PUT" | "DELETE"'
|
|
1496
|
+
};
|
|
1497
|
+
```
|
|
1065
1498
|
|
|
1066
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1559
|
+
```
|
|
1560
|
+
Enum: [
|
|
1561
|
+
o 'ENUM Identifier Block', '["enum", 2, 3]'
|
|
1562
|
+
]
|
|
1563
|
+
```
|
|
1109
1564
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
-
|
|
1670
|
+
Generic parameters on type aliases work the same way:
|
|
1120
1671
|
|
|
1121
1672
|
```coffee
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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:**
|