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/docs/GUIDE.md DELETED
@@ -1,636 +0,0 @@
1
- <p><img src="rip.svg" alt="Rip Logo" width="100"></p>
2
-
3
- # Rip Language Guide
4
-
5
- > **Modern CoffeeScript with Built-in Reactivity**
6
-
7
- This comprehensive guide covers Rip's reactive primitives, special operators, and regex enhancements. Rip provides reactivity as a **language-level construct**, not a library import—state management is built into the syntax itself.
8
-
9
- ---
10
-
11
- ## Table of Contents
12
-
13
- 1. [Reactivity](#1-reactivity)
14
- 2. [Special Operators](#2-special-operators)
15
- 3. [Regex+ Features](#3-regex-features)
16
-
17
- ---
18
-
19
- # 1. Reactivity
20
-
21
- Rip provides reactive primitives as **language-level operators**, not library imports.
22
-
23
- ## Reactive Operators
24
-
25
- | Operator | Name | Read as | Purpose |
26
- |----------|------|---------|---------|
27
- | `=` | Assign | "gets value" | Regular assignment |
28
- | `:=` | State | "**has state**" | Reactive state variable |
29
- | `~=` | Computed | "**always equals**" | Computed value (auto-updates when dependencies change) |
30
- | `~>` | Effect | "**reacts to**" | Side effect that runs when dependencies change |
31
- | `=!` | Readonly | "equals, dammit!" | Constant (`const`) - not reactive, just immutable |
32
-
33
- ## Reactive State (`:=`) — "has state"
34
-
35
- The state operator creates reactive state:
36
-
37
- ```coffee
38
- count := 0 # count has state 0
39
- name := "world" # name has state "world"
40
- ```
41
-
42
- State changes automatically trigger updates in any computed values or effects that depend on them.
43
-
44
- ## Computed Values (`~=`) — "always equals"
45
-
46
- The computed operator creates a value that automatically recomputes when its dependencies change:
47
-
48
- ```coffee
49
- count := 0
50
- doubled ~= count * 2 # doubled always equals count * 2
51
-
52
- count = 5 # doubled automatically becomes 10
53
- count = 10 # doubled automatically becomes 20
54
- ```
55
-
56
- ## Side Effects (`~>`) — "reacts to"
57
-
58
- The effect operator defines a side effect that runs when its dependencies change. Dependencies are auto-tracked from reactive values read in the body:
59
-
60
- ```coffee
61
- count := 0
62
-
63
- ~> console.log "Count changed to:", count
64
-
65
- count = 5 # Logs: "Count changed to: 5"
66
- count = 10 # Logs: "Count changed to: 10"
67
- ```
68
-
69
- Effects are useful for:
70
- - Logging and debugging
71
- - Syncing with external systems
72
- - Analytics tracking
73
- - Local storage persistence
74
-
75
- ### Controllable Effects
76
-
77
- Assign the effect to a variable to control it:
78
-
79
- ```coffee
80
- count := 0
81
-
82
- # Fire and forget (no assignment)
83
- ~> console.log count
84
-
85
- # Controllable (assign to variable)
86
- logger ~> console.log count
87
-
88
- logger.stop! # Pause reactions
89
- logger.run! # Resume reactions
90
- logger.cancel! # Permanent disposal
91
- ```
92
-
93
- ## Constant Values (`=!`) — "equals, dammit!"
94
-
95
- In Rip, regular assignment (`=`) compiles to `let` for maximum flexibility. When you want an immutable constant, use the "equal, dammit!" operator (`=!`), which compiles to `const`:
96
-
97
- ```coffee
98
- # Regular assignment → let (can reassign)
99
- host = "localhost"
100
- host = "example.com" # OK - variables are flexible by default
101
-
102
- # Equal, dammit! → const (can't reassign)
103
- API_URL =! "https://api.example.com"
104
- MAX_RETRIES =! 3
105
-
106
- API_URL = "other" # Error! const cannot be reassigned
107
- ```
108
-
109
- This gives you opt-in immutability when you need it, while keeping the default flexible for scripting.
110
-
111
- ## Auto-Unwrapping
112
-
113
- Reactive variables automatically unwrap in most contexts:
114
-
115
- ```coffee
116
- count := 10
117
-
118
- # All of these work automatically:
119
- doubled ~= count * 2 # Arithmetic
120
- message = "Count: #{count}" # String interpolation
121
- console.log count # Function arguments
122
-
123
- # Explicit access when needed:
124
- count.read() # Get value without tracking dependencies
125
- +count # Unary plus (same as count.value)
126
- ```
127
-
128
- ## Reactive Variable Methods
129
-
130
- | Method | Purpose |
131
- |--------|---------|
132
- | `x.read()` | Get value without tracking (for effects that shouldn't re-run) |
133
- | `x.value` | Direct access to the underlying value |
134
- | `+x` | Shorthand for `x.value` (triggers tracking in computed/effects) |
135
- | `x.lock()` | Make value readonly (can read but can't change) |
136
- | `x.free()` | Unsubscribe from all dependencies (state still works) |
137
- | `x.kill()` | Clean up everything and return final value |
138
-
139
- ## Effect Controller Methods
140
-
141
- When you assign an effect to a variable, you get a controller object:
142
-
143
- | Method | Purpose |
144
- |--------|---------|
145
- | `e.stop!` | Pause reactions (can resume later) |
146
- | `e.run!` | Resume reactions |
147
- | `e.cancel!` | Permanent disposal (cannot resume) |
148
- | `e.active` | Boolean — is the effect running? |
149
-
150
- ## Dependency Tracking
151
-
152
- Understanding when dependencies are tracked is key to effective reactive programming.
153
-
154
- ### What Tracks Dependencies?
155
-
156
- | Expression | Tracks? | Why |
157
- |------------|---------|-----|
158
- | `count * 2` | ✅ Yes | Arithmetic triggers `.valueOf()` |
159
- | `"Count: #{count}"` | ✅ Yes | Interpolation triggers `.toString()` |
160
- | `console.log count` | ✅ Yes | Coercion triggers `.valueOf()` |
161
- | `+count` | ✅ Yes | Unary plus triggers `.valueOf()` |
162
- | `count.value` | ✅ Yes | Direct `.value` access |
163
- | `count.read()` | ❌ No | Explicit non-tracking read |
164
- | `y = count` | ❌ No | Assigns state object, not value |
165
-
166
- ### Example: Tracking vs Non-Tracking
167
-
168
- ```coffee
169
- count := 10
170
-
171
- # Effect A: Subscribes to count (will re-run when count changes)
172
- ~> console.log "A: #{count}"
173
-
174
- # Reading without tracking (for comparisons, etc.)
175
- currentValue = count.read() # Does not create dependency
176
- ```
177
-
178
- ## Lifecycle & Cleanup
179
-
180
- ### Locking a State
181
-
182
- Make a state readonly (subscriptions stay active):
183
-
184
- ```coffee
185
- config := { theme: "dark" }
186
- config.lock()
187
-
188
- config = { theme: "light" } # Silently ignored
189
- config.theme # Still "dark"
190
- ```
191
-
192
- ### Freeing Subscriptions
193
-
194
- Unsubscribe a computed/effect from its dependencies:
195
-
196
- ```coffee
197
- count := 0
198
- doubled ~= count * 2
199
-
200
- doubled.free() # No longer updates when count changes
201
- count = 10 # doubled stays at its last value
202
- ```
203
-
204
- ### Killing a State
205
-
206
- Clean up completely and get the final value:
207
-
208
- ```coffee
209
- count := 10
210
- finalValue = count.kill() # Returns 10, state is now dead
211
-
212
- count = 20 # Error or no-op (state is dead)
213
- ```
214
-
215
- ### Effect Cleanup
216
-
217
- Use the effect controller to manage lifecycle:
218
-
219
- ```coffee
220
- # Assign to a variable for control
221
- ticker ~>
222
- interval = setInterval (-> tick()), 1000
223
- -> clearInterval interval # Cleanup function returned
224
-
225
- # Later, when done:
226
- ticker.cancel! # Stops the effect and runs cleanup
227
- ```
228
-
229
- ## Real-World Example
230
-
231
- A complete reactive counter with persistence:
232
-
233
- ```coffee
234
- # Reactive state — count has state (loaded from localStorage)
235
- count := parseInt(localStorage.getItem("count")) or 0
236
-
237
- # Computed values — always equal to their expressions
238
- doubled ~= count * 2
239
- isEven ~= count % 2 == 0
240
- message ~= "Count is #{count} (#{isEven ? 'even' : 'odd'})"
241
-
242
- # Side effects — react to dependencies (auto-tracked)
243
- ~> localStorage.setItem "count", count # Persist
244
- ~> console.log message # Log
245
-
246
- # Usage
247
- count = 5
248
- # Console: "Count is 5 (odd)"
249
- # localStorage: "5"
250
-
251
- count = 10
252
- # Console: "Count is 10 (even)"
253
- # localStorage: "10"
254
- ```
255
-
256
- ## How It Works
257
-
258
- The Rip compiler transforms reactive operators into efficient JavaScript:
259
-
260
- ```coffee
261
- # Rip source
262
- count := 0 # count has state 0
263
- doubled ~= count * 2 # doubled always equals count * 2
264
- ~> console.log count # reacts to count changes
265
- ```
266
-
267
- ```javascript
268
- // Compiled output (conceptual)
269
- const count = __state(0);
270
- const doubled = __computed(() => count.value * 2);
271
- __effect(() => { console.log(count.value); });
272
- ```
273
-
274
- The runtime is **automatically inlined** - no external dependencies required.
275
-
276
- ## Zero Overhead for Non-Reactive Code
277
-
278
- If your code doesn't use reactive features, no runtime is injected:
279
-
280
- ```coffee
281
- # Non-reactive code
282
- x = 10
283
- y = x * 2
284
- console.log y
285
- ```
286
-
287
- ```javascript
288
- // Clean output - no reactive runtime
289
- let x, y;
290
- x = 10;
291
- y = x * 2;
292
- console.log(y);
293
- ```
294
-
295
- ## Comparison with Other Frameworks
296
-
297
- | Concept | React | Vue | Solid | Rip |
298
- |---------|-------|-----|-------|-----|
299
- | State | `useState()` | `ref()` | `createSignal()` | `x := 0` |
300
- | Computed | `useMemo()` | `computed()` | `createMemo()` | `x ~= y * 2` |
301
- | Effect | `useEffect()` | `watch()` | `createEffect()` | `~> body` or `x ~> body` |
302
- | Constant | `const` | `const` | `const` | `x =! 0` |
303
-
304
- Rip's approach: **No imports, no hooks, no special functions. Just operators.**
305
-
306
- ---
307
-
308
- # 2. Special Operators
309
-
310
- ## Dammit Operator (`!`)
311
-
312
- The **dammit operator (`!`)** is a trailing suffix that does TWO things:
313
- 1. **Calls the function** (even without parentheses)
314
- 2. **Awaits the result** (prepends `await`)
315
-
316
- ### Quick Examples
317
-
318
- ```coffee
319
- # Simple call and await
320
- result = fetchData! # → await fetchData()
321
-
322
- # With arguments
323
- user = getUser!(id) # → await getUser(id)
324
-
325
- # Method calls
326
- data = api.get! # → await api.get()
327
-
328
- # In expressions
329
- total = 5 + getValue! # → 5 + await getValue()
330
- ```
331
-
332
- ### Basic Usage
333
-
334
- ```coffee
335
- # WITHOUT dammit - reference only
336
- fn = loadConfig
337
- typeof fn # → 'function'
338
-
339
- # WITH dammit - calls immediately
340
- config = loadConfig! # → await loadConfig()
341
- ```
342
-
343
- ### Comparison: Before & After
344
-
345
- **Before (Explicit Await):**
346
- ```coffee
347
- user = await db.findUser(id)
348
- posts = await db.getPosts(user.id)
349
- comments = await db.getComments(posts[0].id)
350
- result = await buildResponse(comments)
351
- ```
352
-
353
- **After (Dammit Operator):**
354
- ```coffee
355
- user = db.findUser!(id)
356
- posts = db.getPosts!(user.id)
357
- comments = db.getComments!(posts[0].id)
358
- result = buildResponse!(comments)
359
- ```
360
-
361
- **Benefit:** ~50% shorter, same clarity, **zero performance traps**
362
-
363
- ### Usage Guidelines
364
-
365
- **✅ When to Use `!`:**
366
-
367
- ```coffee
368
- # Sequential async code (most common case)
369
- user = findUser!(id)
370
- posts = getPosts!(user.id)
371
- render!(user, posts)
372
-
373
- # Simple async chains
374
- config = loadConfig!
375
- db = connectDB!(config)
376
- server = startServer!(db)
377
- ```
378
-
379
- **❌ When NOT to Use `!`:**
380
-
381
- ```coffee
382
- # DON'T (serialized - slow):
383
- a = fetch1!
384
- b = fetch2!
385
- c = fetch3!
386
-
387
- # DO (parallel - fast):
388
- [a, b, c] = await Promise.all([fetch1(), fetch2(), fetch3()])
389
- ```
390
-
391
- ## Void Functions (`!` at Definition)
392
-
393
- The `!` at definition suppresses implicit returns (side-effect only functions):
394
-
395
- ```coffee
396
- def processItems!
397
- for item in items
398
- item.update()
399
- # ← Returns undefined, not last expression
400
-
401
- # With explicit return (value stripped)
402
- def validate!(x)
403
- return if x < 0 # → Just "return" (no value)
404
- console.log "valid"
405
- # ← Returns undefined
406
- ```
407
-
408
- **Works with all function types:**
409
- ```coffee
410
- c! = (x) -> # Void thin arrow
411
- x * 2 # Executes but doesn't return value
412
-
413
- process! = (data) => # Void fat arrow
414
- data.toUpperCase() # Executes but returns undefined
415
- ```
416
-
417
- ## Floor Division (`//`)
418
-
419
- True floor division (not just integer division):
420
-
421
- ```coffee
422
- 7 // 3 # → 2
423
- -7 // 3 # → -3 (floors toward negative infinity)
424
- ```
425
-
426
- ## True Modulo (`%%`)
427
-
428
- True mathematical modulo (not remainder like `%`):
429
-
430
- ```coffee
431
- -7 %% 3 # → 2 (always positive)
432
- -7 % 3 # → -1 (remainder, can be negative)
433
- ```
434
-
435
- ## Ternary Operator (`?:`)
436
-
437
- Rip supports both JavaScript-style ternary AND CoffeeScript-style:
438
-
439
- ```coffee
440
- # JavaScript style
441
- status = active ? 'on' : 'off'
442
-
443
- # CoffeeScript style
444
- status = if active then 'on' else 'off'
445
-
446
- # Works with property access, function calls, expressions
447
- result = valid ? obj.field : null
448
- output = ready ? compute() : fallback
449
- value = x > 0 ? x + 1 : 0
450
-
451
- # Nested
452
- level = score > 90 ? 'A' : score > 80 ? 'B' : score > 70 ? 'C' : 'F'
453
- ```
454
-
455
- **Note:** Subscript access in the true branch needs parentheses:
456
-
457
- ```coffee
458
- # Wrap subscript in parens
459
- item = found ? (arr[0]) : default
460
- value = valid ? (data[key]) : null
461
- ```
462
-
463
- **Why possible:** By using `??` for nullish, `?` became available for ternary.
464
-
465
- ## Otherwise Operator (`!?`)
466
-
467
- The otherwise operator handles both null/undefined AND thrown errors:
468
-
469
- ```coffee
470
- result = riskyOperation() !? "default"
471
- # If riskyOperation() throws or returns null/undefined, result = "default"
472
- ```
473
-
474
- ---
475
-
476
- # 3. Regex+ Features
477
-
478
- **Ruby-Inspired Regex Matching with Automatic Capture**
479
-
480
- Rip extends CoffeeScript with two powerful regex features inspired by Ruby: the **`=~` match operator** and **regex indexing**. Both features automatically manage match results in a global `_` variable.
481
-
482
- ## `=~` Match Operator
483
-
484
- ### Syntax
485
-
486
- ```coffee
487
- text =~ /pattern/
488
- ```
489
-
490
- ### Behavior
491
-
492
- - Executes: `(_ = toSearchable(text).match(/pattern/))`
493
- - Stores match result in `_` variable (accessible immediately)
494
- - Returns: the match result (truthy) or `null`
495
-
496
- ### Examples
497
-
498
- **Basic matching:**
499
- ```coffee
500
- text = "hello world"
501
- if text =~ /world/
502
- console.log("Found:", _[0]) # "world"
503
- ```
504
-
505
- **Capture groups:**
506
- ```coffee
507
- email = "user@example.com"
508
- if email =~ /(.+)@(.+)/
509
- username = _[1] # "user"
510
- domain = _[2] # "example.com"
511
- ```
512
-
513
- **Phone number parsing:**
514
- ```coffee
515
- phone = "2125551234"
516
- if phone =~ /^([2-9]\d\d)([2-9]\d\d)(\d{4})$/
517
- formatted = "(#{_[1]}) #{_[2]}-#{_[3]}"
518
- # Result: "(212) 555-1234"
519
- ```
520
-
521
- ## Regex Indexing
522
-
523
- ### Syntax
524
-
525
- ```coffee
526
- value[/pattern/] # Returns full match (capture 0)
527
- value[/pattern/, n] # Returns capture group n
528
- ```
529
-
530
- ### Examples
531
-
532
- **Simple match:**
533
- ```coffee
534
- "steve"[/eve/] # Returns "eve"
535
- ```
536
-
537
- **Capture group:**
538
- ```coffee
539
- "steve"[/e(v)e/, 1] # Returns "v"
540
- ```
541
-
542
- **Email domain:**
543
- ```coffee
544
- domain = "user@example.com"[/@(.+)$/, 1]
545
- # Returns: "example.com"
546
- ```
547
-
548
- ## Combined Usage
549
-
550
- The real power comes from using both features together:
551
-
552
- ```coffee
553
- # Parse, validate, and format in clean steps
554
- email = "Admin@Company.COM"
555
- if email =~ /^([^@]+)@([^@]+)$/i
556
- username = _[1].toLowerCase() # "admin"
557
- domain = _[2].toLowerCase() # "company.com"
558
- "#{username}@#{domain}" # Normalized email
559
- ```
560
-
561
- ## Elegant Validator Pattern
562
-
563
- One of the most powerful use cases is building validators:
564
-
565
- ```coffee
566
- validators =
567
- # Extract and validate in one expression
568
- id: (v) -> v[/^([1-9]\d{0,19})$/] and parseInt(_[1])
569
- email: (v) -> v[/^([^@]+)@([^@]+\.[a-z]{2,})$/i] and _[0]
570
- zip: (v) -> v[/^(\d{5})/] and _[1]
571
- phone: (v) -> v[/^(\d{10})$/] and formatPhone(_[1])
572
-
573
- # Normalize formats
574
- ssn: (v) -> v[/^(\d{3})-?(\d{2})-?(\d{4})$/] and "#{_[1]}#{_[2]}#{_[3]}"
575
- zipplus4: (v) -> v[/^(\d{5})-?(\d{4})$/] and "#{_[1]}-#{_[2]}"
576
-
577
- # Boolean validators with =~
578
- truthy: (v) -> (v =~ /^(true|t|1|yes|y|on)$/i) and true
579
- falsy: (v) -> (v =~ /^(false|f|0|no|n|off)$/i) and true
580
- ```
581
-
582
- **Each validator:**
583
- - Validates format
584
- - Extracts/transforms data
585
- - Returns normalized value or falsy
586
- - **All in one line!**
587
-
588
- ## Heregex (Extended Regular Expressions)
589
-
590
- Rip supports heregexes - extended regular expressions that allow whitespace and comments for readability:
591
-
592
- ```coffee
593
- pattern = ///
594
- ^ \d+ # starts with digits
595
- \s* # optional whitespace
596
- [a-z]+ # followed by letters
597
- $ # end of string
598
- ///i
599
-
600
- # Compiles to: /^\d+\s*[a-z]+$/i
601
- # Comments and whitespace automatically stripped!
602
- ```
603
-
604
- ## Security Features
605
-
606
- ### Injection Protection
607
-
608
- By default, **rejects strings with newlines**:
609
-
610
- ```coffee
611
- # Safe - rejects malicious input
612
- userInput = "test\nmalicious"
613
- userInput =~ /^test$/ # Returns null! (newline detected)
614
-
615
- # Explicit multiline when needed
616
- text = "line1\nline2"
617
- text =~ /line2/m # Works! (/m flag allows newlines)
618
- ```
619
-
620
- ---
621
-
622
- ## Design Philosophy
623
-
624
- 1. **Syntax over API** — Reactive primitives are operators, not function calls
625
- 2. **Implicit tracking** — Dependencies are detected automatically
626
- 3. **Minimal boilerplate** — No `useState`, no `.value` in most cases
627
- 4. **Familiar feel** — Looks like regular assignment, behaves reactively
628
- 5. **Zero dependencies** — Runtime is inlined, no external packages needed
629
- 6. **Framework-agnostic** — Use Rip's reactivity with any UI framework
630
-
631
- ---
632
-
633
- **See Also:**
634
- - [INTERNALS.md](INTERNALS.md) - Compiler and parser details
635
- - [RATIONALE.md](RATIONALE.md) - Why Rip exists
636
- - [BROWSER.md](BROWSER.md) - Browser usage and REPL guide