rip-lang 2.9.2 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -2
- package/README.md +55 -42
- package/docs/RIP-INTERNALS.md +576 -0
- package/docs/RIP-LANG.md +1126 -0
- package/docs/{TYPES.md → RIP-TYPES.md} +3 -3
- package/docs/dist/rip.browser.js +3296 -5482
- package/docs/dist/rip.browser.min.js +248 -332
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/package.json +1 -1
- package/src/browser.js +3 -20
- package/src/compiler.js +1683 -4550
- package/src/grammar/grammar.rip +531 -489
- package/src/lexer.js +1327 -3034
- package/src/parser.js +213 -220
- package/src/repl.js +16 -9
- package/docs/BROWSER.md +0 -990
- package/docs/GUIDE.md +0 -636
- package/docs/INTERNALS.md +0 -857
- package/docs/RATIONALE.md +0 -180
- package/docs/examples/README.md +0 -33
- package/docs/examples/arrows.rip +0 -74
- package/docs/examples/async-await.rip +0 -59
- package/docs/examples/existential.rip +0 -86
- package/docs/examples/fibonacci.rip +0 -12
- package/docs/examples/module.rip +0 -48
- package/docs/examples/ranges.rip +0 -45
- package/docs/examples/reactivity.rip +0 -48
- package/docs/examples/switch.rip +0 -50
- package/docs/examples/ternary.rip +0 -36
- /package/docs/{REACTIVITY.md → RIP-REACTIVITY.md} +0 -0
package/docs/RIP-LANG.md
ADDED
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
# Rip 3.0 Language Reference
|
|
2
|
+
|
|
3
|
+
Rip is a modern reactive language that compiles to ES2022 JavaScript. It combines CoffeeScript's elegant syntax with built-in reactivity primitives. Zero dependencies, self-hosting, ~7,700 LOC.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Installation & Running](#1-installation--running)
|
|
10
|
+
2. [Core Syntax](#2-core-syntax)
|
|
11
|
+
3. [Operators](#3-operators)
|
|
12
|
+
4. [Functions](#4-functions)
|
|
13
|
+
5. [Classes](#5-classes)
|
|
14
|
+
6. [Reactivity](#6-reactivity)
|
|
15
|
+
7. [Async Patterns](#7-async-patterns)
|
|
16
|
+
8. [Modules & Imports](#8-modules--imports)
|
|
17
|
+
9. [Regex Features](#9-regex-features)
|
|
18
|
+
10. [Server-Side Development](#10-server-side-development)
|
|
19
|
+
11. [CLI Tools & Scripts](#11-cli-tools--scripts)
|
|
20
|
+
12. [JavaScript Interop](#12-javascript-interop)
|
|
21
|
+
13. [Common Patterns](#13-common-patterns)
|
|
22
|
+
14. [Quick Reference](#14-quick-reference)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# 1. Installation & Running
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Install Bun first (if needed)
|
|
30
|
+
curl -fsSL https://bun.sh/install | bash
|
|
31
|
+
|
|
32
|
+
# Install Rip globally
|
|
33
|
+
bun add -g rip-lang
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
rip # Interactive REPL
|
|
38
|
+
rip app.rip # Run a file
|
|
39
|
+
rip -c app.rip # Compile to JavaScript (stdout)
|
|
40
|
+
rip -o app.js app.rip # Compile to file
|
|
41
|
+
rip -t app.rip # Show tokens (debug)
|
|
42
|
+
rip -s app.rip # Show S-expressions (debug)
|
|
43
|
+
bun app.rip # Direct execution with Bun loader
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
All Rip files use the `.rip` extension.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
# 2. Core Syntax
|
|
51
|
+
|
|
52
|
+
## Variables
|
|
53
|
+
|
|
54
|
+
```coffee
|
|
55
|
+
# Regular assignment (compiles to let)
|
|
56
|
+
name = "Alice"
|
|
57
|
+
count = 0
|
|
58
|
+
items = [1, 2, 3]
|
|
59
|
+
|
|
60
|
+
# Constant assignment (compiles to const)
|
|
61
|
+
MAX_SIZE =! 100
|
|
62
|
+
API_URL =! "https://api.example.com"
|
|
63
|
+
|
|
64
|
+
# Destructuring
|
|
65
|
+
{name, age} = person
|
|
66
|
+
[first, second, ...rest] = items
|
|
67
|
+
{data: {users}} = response
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Data Types
|
|
71
|
+
|
|
72
|
+
```coffee
|
|
73
|
+
# Strings (interpolation with #{} or ${})
|
|
74
|
+
greeting = "Hello, #{name}!"
|
|
75
|
+
greeting = "Hello, ${name}!"
|
|
76
|
+
|
|
77
|
+
# Heredocs — closing delimiter position defines the left margin
|
|
78
|
+
multiline = """
|
|
79
|
+
This is a
|
|
80
|
+
multi-line string
|
|
81
|
+
"""
|
|
82
|
+
# Result: "This is a\nmulti-line string" (closing """ at col 2 strips 2 spaces)
|
|
83
|
+
|
|
84
|
+
# Numbers
|
|
85
|
+
count = 42
|
|
86
|
+
price = 19.99
|
|
87
|
+
hex = 0xFF
|
|
88
|
+
binary = 0b1010
|
|
89
|
+
|
|
90
|
+
# Arrays
|
|
91
|
+
items = [1, 2, 3]
|
|
92
|
+
matrix = [[1, 2], [3, 4]]
|
|
93
|
+
|
|
94
|
+
# Objects
|
|
95
|
+
user = {name: "Alice", age: 30}
|
|
96
|
+
shorthand = {name, age} # Same as {name: name, age: age}
|
|
97
|
+
|
|
98
|
+
# Ranges
|
|
99
|
+
nums = [1..5] # [1, 2, 3, 4, 5]
|
|
100
|
+
exclusive = [1...5] # [1, 2, 3, 4]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Control Flow
|
|
104
|
+
|
|
105
|
+
```coffee
|
|
106
|
+
# If/else (expression — returns value)
|
|
107
|
+
status = if active then "on" else "off"
|
|
108
|
+
|
|
109
|
+
# Block form
|
|
110
|
+
if user.admin
|
|
111
|
+
showAdminPanel()
|
|
112
|
+
else if user.moderator
|
|
113
|
+
showModPanel()
|
|
114
|
+
else
|
|
115
|
+
showUserPanel()
|
|
116
|
+
|
|
117
|
+
# Ternary (JS-style)
|
|
118
|
+
status = active ? "on" : "off"
|
|
119
|
+
result = x > 5 ? "big" : "small"
|
|
120
|
+
|
|
121
|
+
# NOTE: Subscript in ternary true-branch needs parentheses
|
|
122
|
+
item = found ? (arr[0]) : default
|
|
123
|
+
|
|
124
|
+
# Unless
|
|
125
|
+
showWarning() unless saved
|
|
126
|
+
|
|
127
|
+
# Postfix conditionals
|
|
128
|
+
console.log "active" if user.active
|
|
129
|
+
return early unless valid
|
|
130
|
+
|
|
131
|
+
# Switch/when
|
|
132
|
+
result = switch status
|
|
133
|
+
when "pending" then "Waiting..."
|
|
134
|
+
when "active" then "Running"
|
|
135
|
+
when "done" then "Complete"
|
|
136
|
+
else "Unknown"
|
|
137
|
+
|
|
138
|
+
# Pattern matching in switch
|
|
139
|
+
switch value
|
|
140
|
+
when 1, 2, 3
|
|
141
|
+
"small"
|
|
142
|
+
when 4, 5, 6
|
|
143
|
+
"medium"
|
|
144
|
+
else
|
|
145
|
+
"large"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Guard Clauses
|
|
149
|
+
|
|
150
|
+
Rip supports Ruby-style control flow short-circuits:
|
|
151
|
+
|
|
152
|
+
```coffee
|
|
153
|
+
# or return — return early if falsy
|
|
154
|
+
def loadUser(id)
|
|
155
|
+
data = fetchUser!(id) or return {error: "User not found"}
|
|
156
|
+
processUser(data)
|
|
157
|
+
|
|
158
|
+
# or throw — throw if falsy
|
|
159
|
+
def requireAuth(req)
|
|
160
|
+
token = req.headers.authorization or throw new Error "No auth token"
|
|
161
|
+
verify(token)
|
|
162
|
+
|
|
163
|
+
# ?? return — return only if null/undefined (not falsy values like 0, "")
|
|
164
|
+
def getPort(config)
|
|
165
|
+
port = config.port ?? return 3000 # 0 is valid, won't trigger return
|
|
166
|
+
port
|
|
167
|
+
|
|
168
|
+
# ?? throw — throw only if null/undefined
|
|
169
|
+
def requireId(params)
|
|
170
|
+
id = params.id ?? throw new Error "ID required" # 0 is valid ID
|
|
171
|
+
id
|
|
172
|
+
|
|
173
|
+
# and return — return if truthy (for cache patterns)
|
|
174
|
+
def getData(key)
|
|
175
|
+
cached = cache.get(key) and return cached
|
|
176
|
+
result = compute(key)
|
|
177
|
+
cache.set(key, result)
|
|
178
|
+
result
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Key distinction:**
|
|
182
|
+
- `or`/`and` — check **truthiness** (falsy = `false`, `0`, `""`, `null`, `undefined`)
|
|
183
|
+
- `??` — check **nullish** only (`null`, `undefined`) — `0`, `""`, `false` pass through
|
|
184
|
+
|
|
185
|
+
## Loops
|
|
186
|
+
|
|
187
|
+
```coffee
|
|
188
|
+
# For...in (arrays)
|
|
189
|
+
for item in items
|
|
190
|
+
console.log item
|
|
191
|
+
|
|
192
|
+
# With index
|
|
193
|
+
for item, i in items
|
|
194
|
+
console.log "#{i}: #{item}"
|
|
195
|
+
|
|
196
|
+
# For...of (objects)
|
|
197
|
+
for key, value of object
|
|
198
|
+
console.log "#{key} = #{value}"
|
|
199
|
+
|
|
200
|
+
# For own (skip inherited)
|
|
201
|
+
for own key, value of object
|
|
202
|
+
console.log key
|
|
203
|
+
|
|
204
|
+
# For...as (ES6 for-of on iterables)
|
|
205
|
+
for x as iterable
|
|
206
|
+
console.log x
|
|
207
|
+
|
|
208
|
+
# For...as! (async iteration shorthand)
|
|
209
|
+
for x as! asyncIterable
|
|
210
|
+
console.log x
|
|
211
|
+
# Equivalent to: for await x as asyncIterable
|
|
212
|
+
|
|
213
|
+
# Range loops
|
|
214
|
+
for i in [1..10]
|
|
215
|
+
console.log i
|
|
216
|
+
|
|
217
|
+
# While / Until / Loop
|
|
218
|
+
while condition
|
|
219
|
+
doSomething()
|
|
220
|
+
|
|
221
|
+
until done
|
|
222
|
+
process()
|
|
223
|
+
|
|
224
|
+
loop
|
|
225
|
+
data = fetch()
|
|
226
|
+
break if data.complete
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Comprehensions
|
|
230
|
+
|
|
231
|
+
```coffee
|
|
232
|
+
# Array comprehension (context-aware!)
|
|
233
|
+
squares = (x * x for x in [1..10])
|
|
234
|
+
|
|
235
|
+
# With filter
|
|
236
|
+
evens = (x for x in [1..10] when x % 2 is 0)
|
|
237
|
+
|
|
238
|
+
# Object comprehension
|
|
239
|
+
doubled = {k: v * 2 for k, v of prices}
|
|
240
|
+
|
|
241
|
+
# Statement context (no array created — just loops)
|
|
242
|
+
console.log item for item in items
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Comments
|
|
246
|
+
|
|
247
|
+
```coffee
|
|
248
|
+
# Single line comment
|
|
249
|
+
|
|
250
|
+
###
|
|
251
|
+
Block comment
|
|
252
|
+
Multiple lines
|
|
253
|
+
###
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
# 3. Operators
|
|
259
|
+
|
|
260
|
+
## Standard Operators
|
|
261
|
+
|
|
262
|
+
| Operator | Example | Description |
|
|
263
|
+
|----------|---------|-------------|
|
|
264
|
+
| `+` `-` `*` `/` | `a + b` | Arithmetic |
|
|
265
|
+
| `%` | `a % b` | Remainder (can be negative) |
|
|
266
|
+
| `**` | `a ** b` | Exponentiation |
|
|
267
|
+
| `==` `!=` | `a == b` | Equality (compiles to `===`) |
|
|
268
|
+
| `<` `>` `<=` `>=` | `a < b` | Comparison |
|
|
269
|
+
| `and` `or` `not` | `a and b` | Logical (also `&&` `\|\|` `!`) |
|
|
270
|
+
| `is` `isnt` | `a is b` | Identity (`===` / `!==`) |
|
|
271
|
+
| `in` | `x in arr` | Array membership |
|
|
272
|
+
| `of` | `k of obj` | Object key existence |
|
|
273
|
+
| `?` (postfix) | `a?` | Existence check (`a != null`) |
|
|
274
|
+
| `?` (ternary) | `a ? b : c` | Ternary conditional |
|
|
275
|
+
| `?.` `?.[]` `?.()` | `a?.b` `a?.[0]` `a?.()` | Optional chaining (ES6) |
|
|
276
|
+
| `??` | `a ?? b` | Nullish coalescing |
|
|
277
|
+
|
|
278
|
+
## Rip-Specific Operators
|
|
279
|
+
|
|
280
|
+
| Operator | Name | Example | Compiles To |
|
|
281
|
+
|----------|------|---------|-------------|
|
|
282
|
+
| `=` | Assign | `x = 5` | `let x; x = 5` |
|
|
283
|
+
| `:=` | State | `count := 0` | Reactive state |
|
|
284
|
+
| `~=` | Computed | `doubled ~= count * 2` | Computed value |
|
|
285
|
+
| `=!` | Readonly | `MAX =! 100` | `const MAX = 100` |
|
|
286
|
+
| `//` | Floor division | `7 // 2` | `Math.floor(7 / 2)` — always floors toward negative infinity |
|
|
287
|
+
| `%%` | True modulo | `-1 %% 3` | Always positive result (not remainder) |
|
|
288
|
+
| `!` | Dammit | `fetchData!` | `await fetchData()` — calls AND awaits |
|
|
289
|
+
| `!` | Void | `def process!` | Suppresses implicit return |
|
|
290
|
+
| `!?` | Otherwise | `val !? 5` | Default if undefined or throws |
|
|
291
|
+
| `=~` | Match | `str =~ /pat/` | Ruby-style regex match, captures in `_` |
|
|
292
|
+
|
|
293
|
+
## Assignment Operators
|
|
294
|
+
|
|
295
|
+
```coffee
|
|
296
|
+
x = 5 # let x = 5
|
|
297
|
+
x := 5 # reactive state
|
|
298
|
+
x ~= y * 2 # computed (auto-updates)
|
|
299
|
+
x =! 5 # const x = 5
|
|
300
|
+
x += 1 # x = x + 1
|
|
301
|
+
x -= 1 # x = x - 1
|
|
302
|
+
x *= 2 # x = x * 2
|
|
303
|
+
x /= 2 # x = x / 2
|
|
304
|
+
x //= 2 # x = Math.floor(x / 2)
|
|
305
|
+
x %%= 3 # x = true modulo
|
|
306
|
+
x ?= 10 # x = x ?? 10 (nullish assignment)
|
|
307
|
+
x &&= val # x = x && val
|
|
308
|
+
x ||= val # x = x || val
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Optional Chaining
|
|
312
|
+
|
|
313
|
+
```coffee
|
|
314
|
+
# ES6 optional chaining (dot required)
|
|
315
|
+
user?.profile?.name
|
|
316
|
+
arr?.[0]
|
|
317
|
+
fn?.(arg)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Ternary Operator
|
|
321
|
+
|
|
322
|
+
```coffee
|
|
323
|
+
# JavaScript-style ternary
|
|
324
|
+
status = active ? 'on' : 'off'
|
|
325
|
+
result = valid ? obj.field : null
|
|
326
|
+
output = ready ? compute() : fallback
|
|
327
|
+
|
|
328
|
+
# Nested
|
|
329
|
+
level = score > 90 ? 'A' : score > 80 ? 'B' : score > 70 ? 'C' : 'F'
|
|
330
|
+
|
|
331
|
+
# Subscript in true-branch needs parentheses
|
|
332
|
+
item = found ? (arr[0]) : default
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Otherwise Operator (`!?`)
|
|
336
|
+
|
|
337
|
+
Handles both null/undefined AND thrown errors:
|
|
338
|
+
|
|
339
|
+
```coffee
|
|
340
|
+
result = riskyOperation() !? "default"
|
|
341
|
+
# If riskyOperation() throws or returns null/undefined, result = "default"
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
# 4. Functions
|
|
347
|
+
|
|
348
|
+
## Function Styles
|
|
349
|
+
|
|
350
|
+
```coffee
|
|
351
|
+
# Named function (hoisted)
|
|
352
|
+
def greet(name)
|
|
353
|
+
"Hello, #{name}!"
|
|
354
|
+
|
|
355
|
+
# Arrow function (not hoisted, unbound this)
|
|
356
|
+
add = (a, b) -> a + b
|
|
357
|
+
|
|
358
|
+
# Fat arrow (bound this — use in callbacks/handlers)
|
|
359
|
+
handler = (e) => @process(e)
|
|
360
|
+
|
|
361
|
+
# Void function (suppresses implicit return)
|
|
362
|
+
def logItems!
|
|
363
|
+
for item in items
|
|
364
|
+
console.log item
|
|
365
|
+
# Returns undefined, not last expression
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Void works with all function types:
|
|
369
|
+
|
|
370
|
+
```coffee
|
|
371
|
+
c! = (x) -> # Void thin arrow
|
|
372
|
+
process! = (d) => # Void fat arrow
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Parameters
|
|
376
|
+
|
|
377
|
+
```coffee
|
|
378
|
+
# Default parameters
|
|
379
|
+
def greet(name = "World")
|
|
380
|
+
"Hello, #{name}!"
|
|
381
|
+
|
|
382
|
+
# Rest parameters
|
|
383
|
+
def sum(...nums)
|
|
384
|
+
nums.reduce ((a, b) -> a + b), 0
|
|
385
|
+
|
|
386
|
+
# Destructuring parameters
|
|
387
|
+
def processUser({name, age})
|
|
388
|
+
console.log "#{name} is #{age}"
|
|
389
|
+
|
|
390
|
+
# Constructor shorthand (in classes)
|
|
391
|
+
constructor: (@name, @age) ->
|
|
392
|
+
# Automatically assigns this.name and this.age
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Implicit Returns
|
|
396
|
+
|
|
397
|
+
```coffee
|
|
398
|
+
# Last expression is returned automatically
|
|
399
|
+
def add(a, b)
|
|
400
|
+
a + b # Returns this
|
|
401
|
+
|
|
402
|
+
def getStatus(user)
|
|
403
|
+
if user.active
|
|
404
|
+
"active"
|
|
405
|
+
else
|
|
406
|
+
"inactive"
|
|
407
|
+
|
|
408
|
+
# Explicit return when needed
|
|
409
|
+
def findUser(id)
|
|
410
|
+
for user in users
|
|
411
|
+
return user if user.id is id
|
|
412
|
+
null
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Calling Functions
|
|
416
|
+
|
|
417
|
+
```coffee
|
|
418
|
+
# Normal calls
|
|
419
|
+
greet("Alice")
|
|
420
|
+
add(1, 2)
|
|
421
|
+
|
|
422
|
+
# Without parentheses (when unambiguous)
|
|
423
|
+
console.log "Hello"
|
|
424
|
+
greet "World"
|
|
425
|
+
|
|
426
|
+
# Chained
|
|
427
|
+
users.filter((u) -> u.active).map((u) -> u.name)
|
|
428
|
+
|
|
429
|
+
# Ruby-style constructor
|
|
430
|
+
counter = Counter.new(initial: 5)
|
|
431
|
+
# Same as: new Counter({initial: 5})
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
# 5. Classes
|
|
437
|
+
|
|
438
|
+
```coffee
|
|
439
|
+
class Animal
|
|
440
|
+
constructor: (@name) ->
|
|
441
|
+
|
|
442
|
+
speak: ->
|
|
443
|
+
console.log "#{@name} makes a sound"
|
|
444
|
+
|
|
445
|
+
class Dog extends Animal
|
|
446
|
+
constructor: (name, @breed) ->
|
|
447
|
+
super(name)
|
|
448
|
+
|
|
449
|
+
speak: ->
|
|
450
|
+
console.log "#{@name} barks!"
|
|
451
|
+
|
|
452
|
+
class Counter
|
|
453
|
+
@count = 0 # Static property
|
|
454
|
+
@increment: -> # Static method
|
|
455
|
+
@count += 1
|
|
456
|
+
|
|
457
|
+
# Instantiation
|
|
458
|
+
dog = new Dog("Buddy", "Golden Retriever")
|
|
459
|
+
dog = Dog.new("Buddy", "Golden Retriever") # Ruby-style
|
|
460
|
+
user = User.new(name: "Alice", role: "admin")
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
# 6. Reactivity
|
|
466
|
+
|
|
467
|
+
Rip's reactive features are **language-level operators**, not library imports.
|
|
468
|
+
|
|
469
|
+
## Reactive Operators
|
|
470
|
+
|
|
471
|
+
| Operator | Name | Read as | Purpose |
|
|
472
|
+
|----------|------|---------|---------|
|
|
473
|
+
| `=` | Assign | "gets value" | Regular assignment |
|
|
474
|
+
| `:=` | State | "has state" | Reactive state variable |
|
|
475
|
+
| `~=` | Computed | "always equals" | Computed value (auto-updates) |
|
|
476
|
+
| `~>` | Effect | "reacts to" | Side effect on dependency change |
|
|
477
|
+
| `=!` | Readonly | "equals, dammit!" | Constant (`const`) |
|
|
478
|
+
|
|
479
|
+
## State (`:=`)
|
|
480
|
+
|
|
481
|
+
```coffee
|
|
482
|
+
count := 0
|
|
483
|
+
name := "World"
|
|
484
|
+
items := []
|
|
485
|
+
|
|
486
|
+
# Write triggers updates
|
|
487
|
+
count = 5 # All dependents update
|
|
488
|
+
items = [...items, newItem]
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Computed Values (`~=`)
|
|
492
|
+
|
|
493
|
+
```coffee
|
|
494
|
+
count := 0
|
|
495
|
+
doubled ~= count * 2
|
|
496
|
+
message ~= "Count is #{count}"
|
|
497
|
+
|
|
498
|
+
count = 5
|
|
499
|
+
# doubled is now 10
|
|
500
|
+
# message is now "Count is 5"
|
|
501
|
+
|
|
502
|
+
# Complex computed
|
|
503
|
+
items := [{price: 10}, {price: 20}]
|
|
504
|
+
total ~= items.reduce ((sum, i) -> sum + i.price), 0
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## Effects (`~>`)
|
|
508
|
+
|
|
509
|
+
```coffee
|
|
510
|
+
count := 0
|
|
511
|
+
|
|
512
|
+
# Fire and forget
|
|
513
|
+
~> console.log "Count changed to #{count}"
|
|
514
|
+
|
|
515
|
+
count = 5 # Logs: "Count changed to 5"
|
|
516
|
+
|
|
517
|
+
# Controllable (assign to variable)
|
|
518
|
+
logger ~> console.log count
|
|
519
|
+
logger.stop! # Pause reactions
|
|
520
|
+
logger.run! # Resume reactions
|
|
521
|
+
logger.cancel! # Permanent disposal
|
|
522
|
+
|
|
523
|
+
# With cleanup
|
|
524
|
+
ticker ~>
|
|
525
|
+
interval = setInterval (-> tick()), 1000
|
|
526
|
+
-> clearInterval interval # Cleanup function
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
## Auto-Unwrapping
|
|
530
|
+
|
|
531
|
+
Reactive variables automatically unwrap in most contexts:
|
|
532
|
+
|
|
533
|
+
```coffee
|
|
534
|
+
count := 10
|
|
535
|
+
|
|
536
|
+
# All of these work automatically:
|
|
537
|
+
doubled ~= count * 2 # Arithmetic
|
|
538
|
+
message = "Count: #{count}" # Interpolation
|
|
539
|
+
console.log count # Function arguments
|
|
540
|
+
|
|
541
|
+
# Explicit access when needed:
|
|
542
|
+
count.read() # Get value without tracking dependencies
|
|
543
|
+
+count # Unary plus (same as count.value)
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
## Dependency Tracking
|
|
547
|
+
|
|
548
|
+
| Expression | Tracks? | Why |
|
|
549
|
+
|------------|---------|-----|
|
|
550
|
+
| `count * 2` | Yes | Arithmetic triggers `.valueOf()` |
|
|
551
|
+
| `"Count: #{count}"` | Yes | Interpolation triggers `.toString()` |
|
|
552
|
+
| `console.log count` | Yes | Coercion triggers `.valueOf()` |
|
|
553
|
+
| `count.value` | Yes | Direct `.value` access |
|
|
554
|
+
| `count.read()` | No | Explicit non-tracking read |
|
|
555
|
+
| `y = count` | No | Assigns state object, not value |
|
|
556
|
+
|
|
557
|
+
## Reactive Variable Methods
|
|
558
|
+
|
|
559
|
+
| Method | Purpose |
|
|
560
|
+
|--------|---------|
|
|
561
|
+
| `x.read()` | Get value without tracking |
|
|
562
|
+
| `x.value` | Direct access to underlying value |
|
|
563
|
+
| `+x` | Shorthand for `x.value` |
|
|
564
|
+
| `x.lock()` | Make readonly (subscriptions stay active) |
|
|
565
|
+
| `x.free()` | Unsubscribe from dependencies |
|
|
566
|
+
| `x.kill()` | Clean up everything, return final value |
|
|
567
|
+
|
|
568
|
+
## Effect Controller Methods
|
|
569
|
+
|
|
570
|
+
| Method | Purpose |
|
|
571
|
+
|--------|---------|
|
|
572
|
+
| `e.stop!` | Pause reactions (can resume) |
|
|
573
|
+
| `e.run!` | Resume reactions |
|
|
574
|
+
| `e.cancel!` | Permanent disposal |
|
|
575
|
+
| `e.active` | Boolean — is the effect running? |
|
|
576
|
+
|
|
577
|
+
## When to Use What
|
|
578
|
+
|
|
579
|
+
| Need | Use | Example |
|
|
580
|
+
|------|-----|---------|
|
|
581
|
+
| Mutable state that triggers updates | `:=` | `count := 0` |
|
|
582
|
+
| Computed value from other state | `~=` | `total ~= price * qty` |
|
|
583
|
+
| Side effect on change | `~>` | `~> save(data)` |
|
|
584
|
+
| Controllable side effect | `x ~>` | `saver ~> save(data)` |
|
|
585
|
+
| Immutable constant | `=!` | `API_URL =! "..."` |
|
|
586
|
+
| Regular variable | `=` | `temp = calculate()` |
|
|
587
|
+
|
|
588
|
+
## How It Works
|
|
589
|
+
|
|
590
|
+
```coffee
|
|
591
|
+
# Rip source
|
|
592
|
+
count := 0
|
|
593
|
+
doubled ~= count * 2
|
|
594
|
+
~> console.log count
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
```javascript
|
|
598
|
+
// Compiled output (conceptual)
|
|
599
|
+
const count = __state(0);
|
|
600
|
+
const doubled = __computed(() => count.value * 2);
|
|
601
|
+
__effect(() => { console.log(count.value); });
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
The reactive runtime is **automatically inlined** when needed. Non-reactive code produces clean output with no runtime overhead.
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
# 7. Async Patterns
|
|
609
|
+
|
|
610
|
+
## The Dammit Operator (`!`)
|
|
611
|
+
|
|
612
|
+
The `!` suffix **calls AND awaits** a function:
|
|
613
|
+
|
|
614
|
+
```coffee
|
|
615
|
+
# Without dammit
|
|
616
|
+
user = await getUser(id)
|
|
617
|
+
posts = await getPosts(user.id)
|
|
618
|
+
|
|
619
|
+
# With dammit
|
|
620
|
+
user = getUser!(id)
|
|
621
|
+
posts = getPosts!(user.id)
|
|
622
|
+
|
|
623
|
+
# No arguments — still calls
|
|
624
|
+
data = fetchLatest!
|
|
625
|
+
# Compiles to: await fetchLatest()
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
## Auto-Async Detection
|
|
629
|
+
|
|
630
|
+
Functions containing `await` or `!` are automatically async:
|
|
631
|
+
|
|
632
|
+
```coffee
|
|
633
|
+
def loadUserData(id)
|
|
634
|
+
user = getUser!(id)
|
|
635
|
+
posts = getPosts!(user.id)
|
|
636
|
+
friends = getFriends!(user.id)
|
|
637
|
+
{user, posts, friends}
|
|
638
|
+
# Compiles to: async function loadUserData(id) { ... }
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
## Async Patterns
|
|
642
|
+
|
|
643
|
+
```coffee
|
|
644
|
+
# Sequential (use when order matters)
|
|
645
|
+
def processSequential(ids)
|
|
646
|
+
for id in ids
|
|
647
|
+
result = process!(id)
|
|
648
|
+
console.log result
|
|
649
|
+
|
|
650
|
+
# Parallel (use for independent operations)
|
|
651
|
+
def processParallel(ids)
|
|
652
|
+
results = await Promise.all(ids.map (id) -> process(id))
|
|
653
|
+
results
|
|
654
|
+
|
|
655
|
+
# Error handling
|
|
656
|
+
def safeFetch(url)
|
|
657
|
+
try
|
|
658
|
+
response = fetch!(url)
|
|
659
|
+
response.json!
|
|
660
|
+
catch error
|
|
661
|
+
console.error "Failed:", error
|
|
662
|
+
null
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
## Async Iteration
|
|
666
|
+
|
|
667
|
+
```coffee
|
|
668
|
+
# Long form
|
|
669
|
+
for await x as iterable
|
|
670
|
+
console.log x
|
|
671
|
+
|
|
672
|
+
# Shorthand with as!
|
|
673
|
+
for x as! iterable
|
|
674
|
+
console.log x
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
# 8. Modules & Imports
|
|
680
|
+
|
|
681
|
+
```coffee
|
|
682
|
+
# Named imports
|
|
683
|
+
import { readFile, writeFile } from "fs"
|
|
684
|
+
|
|
685
|
+
# Default import
|
|
686
|
+
import express from "express"
|
|
687
|
+
|
|
688
|
+
# Namespace import
|
|
689
|
+
import * as path from "path"
|
|
690
|
+
|
|
691
|
+
# Mixed
|
|
692
|
+
import React, { useState } from "react"
|
|
693
|
+
|
|
694
|
+
# Relative paths
|
|
695
|
+
import { utils } from "./utils.rip"
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
```coffee
|
|
699
|
+
# Named exports
|
|
700
|
+
export def processData(data)
|
|
701
|
+
data.map (x) -> x * 2
|
|
702
|
+
|
|
703
|
+
export config = {
|
|
704
|
+
timeout: 5000
|
|
705
|
+
retries: 3
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export class DataProcessor
|
|
709
|
+
process: (data) -> data
|
|
710
|
+
|
|
711
|
+
# Default export
|
|
712
|
+
export default {
|
|
713
|
+
process: processData
|
|
714
|
+
config
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
# 9. Regex Features
|
|
721
|
+
|
|
722
|
+
## Match Operator (`=~`)
|
|
723
|
+
|
|
724
|
+
```coffee
|
|
725
|
+
# Basic matching — captures stored in _
|
|
726
|
+
if text =~ /pattern/
|
|
727
|
+
console.log "Found:", _[0]
|
|
728
|
+
|
|
729
|
+
# Capture groups
|
|
730
|
+
email = "user@example.com"
|
|
731
|
+
if email =~ /(.+)@(.+)/
|
|
732
|
+
username = _[1] # "user"
|
|
733
|
+
domain = _[2] # "example.com"
|
|
734
|
+
|
|
735
|
+
# Phone parsing
|
|
736
|
+
phone = "2125551234"
|
|
737
|
+
if phone =~ /^(\d{3})(\d{3})(\d{4})$/
|
|
738
|
+
formatted = "(#{_[1]}) #{_[2]}-#{_[3]}"
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
## Regex Indexing
|
|
742
|
+
|
|
743
|
+
```coffee
|
|
744
|
+
# Extract match directly
|
|
745
|
+
domain = "user@example.com"[/@(.+)$/, 1] # "example.com"
|
|
746
|
+
word = "hello world"[/\w+/] # "hello"
|
|
747
|
+
zip = "12345-6789"[/^(\d{5})/, 1] # "12345"
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
## Heregex (Extended Regex)
|
|
751
|
+
|
|
752
|
+
```coffee
|
|
753
|
+
pattern = ///
|
|
754
|
+
^ # Start
|
|
755
|
+
(\d{3}) # Area code
|
|
756
|
+
[-.\s]? # Optional separator
|
|
757
|
+
(\d{3}) # Exchange
|
|
758
|
+
[-.\s]? # Optional separator
|
|
759
|
+
(\d{4}) # Subscriber
|
|
760
|
+
$ # End
|
|
761
|
+
///
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
## Validator Pattern
|
|
765
|
+
|
|
766
|
+
```coffee
|
|
767
|
+
validators =
|
|
768
|
+
email: (v) -> v[/^([^@]+)@([^@]+\.[a-z]{2,})$/i] and _[0]
|
|
769
|
+
phone: (v) -> v[/^(\d{10})$/] and _[1]
|
|
770
|
+
zip: (v) -> v[/^(\d{5})/] and _[1]
|
|
771
|
+
ssn: (v) -> v[/^(\d{3})-?(\d{2})-?(\d{4})$/] and "#{_[1]}#{_[2]}#{_[3]}"
|
|
772
|
+
truthy: (v) -> (v =~ /^(true|t|1|yes|y|on)$/i) and true
|
|
773
|
+
falsy: (v) -> (v =~ /^(false|f|0|no|n|off)$/i) and true
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
## Security
|
|
777
|
+
|
|
778
|
+
By default, `=~` rejects strings with newlines to prevent injection:
|
|
779
|
+
|
|
780
|
+
```coffee
|
|
781
|
+
userInput = "test\nmalicious"
|
|
782
|
+
userInput =~ /^test$/ # Returns null (newline detected)
|
|
783
|
+
|
|
784
|
+
# Explicit multiline when needed
|
|
785
|
+
text = "line1\nline2"
|
|
786
|
+
text =~ /line2/m # Works with /m flag
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
# 10. Server-Side Development
|
|
792
|
+
|
|
793
|
+
## HTTP Server
|
|
794
|
+
|
|
795
|
+
```coffee
|
|
796
|
+
import { serve } from "bun"
|
|
797
|
+
|
|
798
|
+
serve
|
|
799
|
+
port: 3000
|
|
800
|
+
fetch: (req) ->
|
|
801
|
+
url = new URL(req.url)
|
|
802
|
+
switch url.pathname
|
|
803
|
+
when "/"
|
|
804
|
+
new Response("Hello from Rip!")
|
|
805
|
+
when "/api/users"
|
|
806
|
+
Response.json([{id: 1, name: "Alice"}])
|
|
807
|
+
else
|
|
808
|
+
new Response("Not Found", status: 404)
|
|
809
|
+
|
|
810
|
+
console.log "Server running on http://localhost:3000"
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
## REST API
|
|
814
|
+
|
|
815
|
+
```coffee
|
|
816
|
+
import { serve } from "bun"
|
|
817
|
+
|
|
818
|
+
db = {
|
|
819
|
+
users: [
|
|
820
|
+
{id: 1, name: "Alice", email: "alice@example.com"}
|
|
821
|
+
{id: 2, name: "Bob", email: "bob@example.com"}
|
|
822
|
+
]
|
|
823
|
+
nextId: 3
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
def parseBody(req)
|
|
827
|
+
try
|
|
828
|
+
req.json!
|
|
829
|
+
catch
|
|
830
|
+
null
|
|
831
|
+
|
|
832
|
+
serve
|
|
833
|
+
port: 3000
|
|
834
|
+
fetch: (req) ->
|
|
835
|
+
{pathname} = new URL(req.url)
|
|
836
|
+
method = req.method
|
|
837
|
+
|
|
838
|
+
switch "#{method} #{pathname}"
|
|
839
|
+
when "GET /api/users"
|
|
840
|
+
Response.json(db.users)
|
|
841
|
+
|
|
842
|
+
when /^GET \/api\/users\/(\d+)$/
|
|
843
|
+
id = parseInt(_[1])
|
|
844
|
+
user = db.users.find (u) -> u.id is id
|
|
845
|
+
if user then Response.json(user)
|
|
846
|
+
else Response.json({error: "Not found"}, status: 404)
|
|
847
|
+
|
|
848
|
+
when "POST /api/users"
|
|
849
|
+
body = parseBody!(req)
|
|
850
|
+
user = {id: db.nextId++, ...body}
|
|
851
|
+
db.users.push(user)
|
|
852
|
+
Response.json(user, status: 201)
|
|
853
|
+
|
|
854
|
+
else
|
|
855
|
+
Response.json({error: "Not found"}, status: 404)
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
## WebSocket Server
|
|
859
|
+
|
|
860
|
+
```coffee
|
|
861
|
+
import { serve } from "bun"
|
|
862
|
+
|
|
863
|
+
clients = new Set()
|
|
864
|
+
|
|
865
|
+
serve
|
|
866
|
+
port: 3000
|
|
867
|
+
fetch: (req, server) ->
|
|
868
|
+
if server.upgrade(req)
|
|
869
|
+
return
|
|
870
|
+
new Response("WebSocket server")
|
|
871
|
+
|
|
872
|
+
websocket:
|
|
873
|
+
open: (ws) ->
|
|
874
|
+
clients.add(ws)
|
|
875
|
+
console.log "Client connected (#{clients.size} total)"
|
|
876
|
+
|
|
877
|
+
message: (ws, message) ->
|
|
878
|
+
for client in clients
|
|
879
|
+
client.send(message) unless client is ws
|
|
880
|
+
|
|
881
|
+
close: (ws) ->
|
|
882
|
+
clients.delete(ws)
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
---
|
|
886
|
+
|
|
887
|
+
# 11. CLI Tools & Scripts
|
|
888
|
+
|
|
889
|
+
```coffee
|
|
890
|
+
# Basic CLI tool
|
|
891
|
+
import { argv } from "process"
|
|
892
|
+
|
|
893
|
+
args = argv.slice(2)
|
|
894
|
+
|
|
895
|
+
if args.length is 0
|
|
896
|
+
console.log "Usage: rip greet.rip <name>"
|
|
897
|
+
process.exit(1)
|
|
898
|
+
|
|
899
|
+
name = args[0]
|
|
900
|
+
console.log "Hello, #{name}!"
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
```coffee
|
|
904
|
+
# File processing
|
|
905
|
+
import { readFileSync, writeFileSync, readdirSync } from "fs"
|
|
906
|
+
import { join, extname } from "path"
|
|
907
|
+
|
|
908
|
+
INPUT_DIR =! "./input"
|
|
909
|
+
OUTPUT_DIR =! "./output"
|
|
910
|
+
|
|
911
|
+
files = readdirSync(INPUT_DIR).filter (f) -> extname(f) is ".txt"
|
|
912
|
+
|
|
913
|
+
for filename in files
|
|
914
|
+
content = readFileSync(join(INPUT_DIR, filename), "utf-8")
|
|
915
|
+
processed = content.split("\n").filter((l) -> l.trim().length > 0).join("\n")
|
|
916
|
+
writeFileSync(join(OUTPUT_DIR, filename), processed)
|
|
917
|
+
console.log "Processed: #{filename}"
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
---
|
|
921
|
+
|
|
922
|
+
# 12. JavaScript Interop
|
|
923
|
+
|
|
924
|
+
```coffee
|
|
925
|
+
# Import any npm package
|
|
926
|
+
import express from "express"
|
|
927
|
+
import { z } from "zod"
|
|
928
|
+
import axios from "axios"
|
|
929
|
+
|
|
930
|
+
# JavaScript functions work directly
|
|
931
|
+
console.log("Hello")
|
|
932
|
+
Math.max(1, 2, 3)
|
|
933
|
+
JSON.stringify({a: 1})
|
|
934
|
+
Object.keys(obj)
|
|
935
|
+
|
|
936
|
+
# DOM APIs (in browser)
|
|
937
|
+
document.getElementById("app")
|
|
938
|
+
element.addEventListener "click", handler
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
```javascript
|
|
942
|
+
// Call Rip from JavaScript
|
|
943
|
+
import { processData } from "./utils.rip";
|
|
944
|
+
|
|
945
|
+
// Or compile at runtime
|
|
946
|
+
import { compile } from "rip-lang";
|
|
947
|
+
const { code } = compile('x = 42');
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
# 13. Common Patterns
|
|
953
|
+
|
|
954
|
+
## Error Handling
|
|
955
|
+
|
|
956
|
+
```coffee
|
|
957
|
+
try
|
|
958
|
+
data = fetchData!(url)
|
|
959
|
+
process(data)
|
|
960
|
+
catch error
|
|
961
|
+
console.error "Failed:", error.message
|
|
962
|
+
finally
|
|
963
|
+
cleanup()
|
|
964
|
+
|
|
965
|
+
# Otherwise operator for defaults
|
|
966
|
+
value = riskyOperation() !? "default"
|
|
967
|
+
|
|
968
|
+
# Optional chaining for safety
|
|
969
|
+
name = user?.profile?.name ?? "Anonymous"
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
## Configuration
|
|
973
|
+
|
|
974
|
+
```coffee
|
|
975
|
+
export default
|
|
976
|
+
api:
|
|
977
|
+
baseUrl: process.env.API_URL ?? "http://localhost:3000"
|
|
978
|
+
timeout: parseInt(process.env.TIMEOUT) or 5000
|
|
979
|
+
database:
|
|
980
|
+
host: process.env.DB_HOST ?? "localhost"
|
|
981
|
+
port: parseInt(process.env.DB_PORT) or 5432
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
## Builder Pattern
|
|
985
|
+
|
|
986
|
+
```coffee
|
|
987
|
+
class QueryBuilder
|
|
988
|
+
constructor: ->
|
|
989
|
+
@_select = "*"
|
|
990
|
+
@_from = null
|
|
991
|
+
@_where = []
|
|
992
|
+
@_limit = null
|
|
993
|
+
|
|
994
|
+
select: (fields) -> (@_select = fields; @)
|
|
995
|
+
from: (table) -> (@_from = table; @)
|
|
996
|
+
where: (condition) -> (@_where.push(condition); @)
|
|
997
|
+
limit: (n) -> (@_limit = n; @)
|
|
998
|
+
|
|
999
|
+
build: ->
|
|
1000
|
+
sql = "SELECT #{@_select} FROM #{@_from}"
|
|
1001
|
+
sql += " WHERE #{@_where.join(' AND ')}" if @_where.length
|
|
1002
|
+
sql += " LIMIT #{@_limit}" if @_limit
|
|
1003
|
+
sql
|
|
1004
|
+
|
|
1005
|
+
query = new QueryBuilder()
|
|
1006
|
+
.select("id, name")
|
|
1007
|
+
.from("users")
|
|
1008
|
+
.where("active = true")
|
|
1009
|
+
.limit(10)
|
|
1010
|
+
.build()
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
## Event Emitter
|
|
1014
|
+
|
|
1015
|
+
```coffee
|
|
1016
|
+
class EventEmitter
|
|
1017
|
+
constructor: ->
|
|
1018
|
+
@_listeners = {}
|
|
1019
|
+
|
|
1020
|
+
on: (event, callback) ->
|
|
1021
|
+
@_listeners[event] ?= []
|
|
1022
|
+
@_listeners[event].push(callback)
|
|
1023
|
+
@
|
|
1024
|
+
|
|
1025
|
+
emit: (event, ...args) ->
|
|
1026
|
+
return @ unless @_listeners[event]
|
|
1027
|
+
for callback in @_listeners[event]
|
|
1028
|
+
callback(...args)
|
|
1029
|
+
@
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
---
|
|
1033
|
+
|
|
1034
|
+
# 14. Quick Reference
|
|
1035
|
+
|
|
1036
|
+
## Syntax Cheat Sheet
|
|
1037
|
+
|
|
1038
|
+
```coffee
|
|
1039
|
+
# Variables
|
|
1040
|
+
x = 5 # let
|
|
1041
|
+
x =! 5 # const
|
|
1042
|
+
x := 5 # state (reactive)
|
|
1043
|
+
x ~= y * 2 # computed (reactive)
|
|
1044
|
+
|
|
1045
|
+
# Functions
|
|
1046
|
+
def fn(a, b) # named function
|
|
1047
|
+
a + b
|
|
1048
|
+
fn = (a) -> a # arrow (unbound this)
|
|
1049
|
+
fn = (a) => a # fat arrow (bound this)
|
|
1050
|
+
def fn! # void function
|
|
1051
|
+
|
|
1052
|
+
# Control
|
|
1053
|
+
if x then a else b
|
|
1054
|
+
x ? a : b
|
|
1055
|
+
switch x
|
|
1056
|
+
when 1 then "one"
|
|
1057
|
+
else "other"
|
|
1058
|
+
|
|
1059
|
+
# Loops
|
|
1060
|
+
for x in arr
|
|
1061
|
+
for k, v of obj
|
|
1062
|
+
for x as iterable
|
|
1063
|
+
for x as! asyncIterable
|
|
1064
|
+
while cond
|
|
1065
|
+
until cond
|
|
1066
|
+
|
|
1067
|
+
# Classes
|
|
1068
|
+
class X extends Y
|
|
1069
|
+
constructor: (@a) ->
|
|
1070
|
+
method: -> @a
|
|
1071
|
+
X.new(a: 1)
|
|
1072
|
+
|
|
1073
|
+
# Operators
|
|
1074
|
+
a! # await a()
|
|
1075
|
+
a !? b # a if defined, else b
|
|
1076
|
+
a // b # floor divide
|
|
1077
|
+
a %% b # true modulo
|
|
1078
|
+
a =~ /pat/ # regex match, captures in _
|
|
1079
|
+
a[/pat/, 1] # regex extract
|
|
1080
|
+
a? # existence check (a != null)
|
|
1081
|
+
a ?? b # nullish coalescing
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
## File Templates
|
|
1085
|
+
|
|
1086
|
+
### API Server
|
|
1087
|
+
|
|
1088
|
+
```coffee
|
|
1089
|
+
import { serve } from "bun"
|
|
1090
|
+
|
|
1091
|
+
serve
|
|
1092
|
+
port: 3000
|
|
1093
|
+
fetch: (req) ->
|
|
1094
|
+
Response.json({message: "Hello!"})
|
|
1095
|
+
|
|
1096
|
+
console.log "Running on http://localhost:3000"
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
### Utility Module
|
|
1100
|
+
|
|
1101
|
+
```coffee
|
|
1102
|
+
export def formatDate(date)
|
|
1103
|
+
date.toISOString().split("T")[0]
|
|
1104
|
+
|
|
1105
|
+
export def capitalize(str)
|
|
1106
|
+
str.charAt(0).toUpperCase() + str.slice(1)
|
|
1107
|
+
|
|
1108
|
+
export def sleep(ms)
|
|
1109
|
+
new Promise (resolve) -> setTimeout(resolve, ms)
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
### Reactive State
|
|
1113
|
+
|
|
1114
|
+
```coffee
|
|
1115
|
+
count := 0
|
|
1116
|
+
doubled ~= count * 2
|
|
1117
|
+
|
|
1118
|
+
~> console.log "Count: #{count}, Doubled: #{doubled}"
|
|
1119
|
+
|
|
1120
|
+
count = 5 # Logs: "Count: 5, Doubled: 10"
|
|
1121
|
+
count = 10 # Logs: "Count: 10, Doubled: 20"
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
---
|
|
1125
|
+
|
|
1126
|
+
*Rip 3.0.0 — 1,048 tests passing — Zero dependencies — Self-hosting — ~7,700 LOC*
|