rip-lang 2.9.0 → 2.9.2

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 CHANGED
@@ -7,6 +7,45 @@ All notable changes to Rip will be documented in this file.
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
+ ## [2.9.0] - 2026-02-05
11
+
12
+ ### Compiler Fix
13
+
14
+ - **Interpolated string object keys**: `{"#{k}": v}` now correctly compiles to `{[\`${k}\`]: v}` instead of invalid `{\`${k}\`: v}`. Template literals in object key position are wrapped in computed property syntax.
15
+
16
+ ### @rip-lang/db — DuckDB Server with Official UI
17
+
18
+ Major milestone: complete DuckDB HTTP server with the official DuckDB UI.
19
+
20
+ - **Pure Bun FFI driver** — Direct calls to DuckDB's C API, no npm packages, no Zig
21
+ - **Modern chunk-based API** — Uses `duckdb_fetch_chunk` + `duckdb_vector_get_data` for direct columnar memory reads. No deprecated `duckdb_value_*` functions.
22
+ - **DuckDB UI loads instantly** — Full binary serialization protocol, proper COOP/COEP headers, SSE keepalive, version handshake
23
+ - **Complete type support** — DECIMAL (exact precision), ENUM (dictionary lookup), UUID (native hugeint), LIST/STRUCT/MAP (child vector traversal), all timestamp variants
24
+ - **Timestamp policy** — All timestamps normalized to UTC Date objects; TIMESTAMPTZ recommended
25
+ - **Binary protocol** — Native 16-byte UUID serialization, uint64-aligned validity bitmaps, proper TypeInfo encoding
26
+
27
+ ### @rip-lang/api — v1.0.0
28
+
29
+ - Published as stable 1.0.0
30
+ - Polished README with feature table and cross-references
31
+
32
+ ### @rip-lang/server — v1.0.0
33
+
34
+ - Published as stable 1.0.0
35
+ - Added proper dependencies (rip-lang + @rip-lang/api)
36
+ - Polished README with feature table and cross-references
37
+
38
+ ### Housekeeping
39
+
40
+ - Removed all Zig code and npm DuckDB dependency from @rip-lang/db
41
+ - Consolidated documentation (DEBUGGING.md + INTERNALS.md → README.md)
42
+ - Dynamic platform detection for DuckDB UI headers
43
+ - Support both `--port=N` and `--port N` argument formats
44
+ - Updated all package dependencies to ^2.9.0
45
+ - Removed bun.lock from git tracking
46
+
47
+ ---
48
+
10
49
  ## [2.7.2] - 2026-02-03
11
50
 
12
51
  ### Clean ES Module REPL
package/README.md CHANGED
@@ -72,6 +72,16 @@ class Dog extends Animal
72
72
  dog = Dog.new("Buddy") # Ruby-style constructor
73
73
  ```
74
74
 
75
+ ### String Interpolation
76
+
77
+ ```coffee
78
+ "Hello, #{name}!" # CoffeeScript-style
79
+ "Hello, ${name}!" # JavaScript-style
80
+ "#{a} + #{b} = #{a + b}" # Expressions work in both
81
+ ```
82
+
83
+ Both `#{}` and `${}` compile to JavaScript template literals. Use whichever you prefer.
84
+
75
85
  ### Destructuring & Comprehensions
76
86
 
77
87
  ```coffee
@@ -136,15 +146,35 @@ State, computed values, and effects as language operators:
136
146
 
137
147
  ### Heredoc & Heregex
138
148
 
139
- **Heredoc** — The closing `'''` position sets the left margin (smart dedent):
149
+ **Heredoc** — The closing `'''` or `"""` position defines the left margin. All content is dedented relative to the column where the closing delimiter sits:
140
150
 
141
151
  ```coffee
152
+ html = '''
153
+ <div>
154
+ <p>Hello</p>
155
+ </div>
156
+ '''
157
+ # Closing ''' is at column 4 (same as content) → no leading whitespace
158
+ # Result: "<div>\n <p>Hello</p>\n</div>"
159
+
142
160
  html = '''
143
161
  <div>
144
162
  <p>Hello</p>
145
163
  </div>
146
164
  '''
147
- # Result: " <div>\n <p>Hello</p>\n </div>" (note the leading 2 spaces)
165
+ # Closing ''' is at column 2 (2 less than content) → 2 spaces of leading whitespace
166
+ # Result: " <div>\n <p>Hello</p>\n </div>"
167
+ ```
168
+
169
+ Use `"""` for interpolation, `'''` for literal strings. The same margin rule applies to both:
170
+
171
+ ```coffee
172
+ name = "World"
173
+ msg = """
174
+ Hello, #{name}!
175
+ Welcome to Rip.
176
+ """
177
+ # Result: "Hello, World!\nWelcome to Rip."
148
178
  ```
149
179
 
150
180
  **Heregex** — Extended regex with comments and whitespace:
package/docs/BROWSER.md CHANGED
@@ -166,7 +166,7 @@ Variables:
166
166
  Split-pane editor showing real-time compilation:
167
167
 
168
168
  **Left Pane (Rip):**
169
- ```rip
169
+ ```coffee
170
170
  def fibonacci(n)
171
171
  if n <= 1
172
172
  n
@@ -335,7 +335,7 @@ Build tools, playgrounds, or educational apps:
335
335
  ### All Rip Features Available
336
336
 
337
337
  **✅ Modern Syntax:**
338
- ```rip
338
+ ```coffee
339
339
  # Destructuring
340
340
  {name, age} = person
341
341
  [first, ...rest] = array
@@ -357,7 +357,7 @@ pattern = ///
357
357
  ```
358
358
 
359
359
  **✅ Ruby-Style Regex:**
360
- ```rip
360
+ ```coffee
361
361
  # Match operator
362
362
  email =~ /(.+)@(.+)/
363
363
  domain = _[2]
@@ -367,7 +367,7 @@ zip = "12345-6789"[/^(\d{5})/, 1] # "12345"
367
367
  ```
368
368
 
369
369
  **✅ Functions:**
370
- ```rip
370
+ ```coffee
371
371
  # Three styles
372
372
  def add(a, b) # Hoisted
373
373
  a + b
@@ -380,7 +380,7 @@ divide = (a, b) => # Bound this
380
380
  ```
381
381
 
382
382
  **✅ Classes:**
383
- ```rip
383
+ ```coffee
384
384
  class Person
385
385
  constructor: (@name, @age) ->
386
386
 
@@ -389,7 +389,7 @@ class Person
389
389
  ```
390
390
 
391
391
  **✅ Async/Await:**
392
- ```rip
392
+ ```coffee
393
393
  # Auto-detected!
394
394
  fetchData = ->
395
395
  response = await fetch(url)
@@ -402,7 +402,7 @@ getData = ->
402
402
  ```
403
403
 
404
404
  **✅ String Interpolation:**
405
- ```rip
405
+ ```coffee
406
406
  name = "World"
407
407
  message = "Hello, #{name}!"
408
408
 
@@ -576,7 +576,7 @@ Real-time data processing with clean syntax:
576
576
 
577
577
  The `toSearchable()` helper includes injection protection:
578
578
 
579
- ```rip
579
+ ```coffee
580
580
  # Safe by default - rejects newlines
581
581
  userInput =~ /^[a-z]+$/ # Returns null if input has \n
582
582
 
package/docs/GUIDE.md CHANGED
@@ -483,7 +483,7 @@ Rip extends CoffeeScript with two powerful regex features inspired by Ruby: the
483
483
 
484
484
  ### Syntax
485
485
 
486
- ```rip
486
+ ```coffee
487
487
  text =~ /pattern/
488
488
  ```
489
489
 
@@ -496,14 +496,14 @@ text =~ /pattern/
496
496
  ### Examples
497
497
 
498
498
  **Basic matching:**
499
- ```rip
499
+ ```coffee
500
500
  text = "hello world"
501
501
  if text =~ /world/
502
502
  console.log("Found:", _[0]) # "world"
503
503
  ```
504
504
 
505
505
  **Capture groups:**
506
- ```rip
506
+ ```coffee
507
507
  email = "user@example.com"
508
508
  if email =~ /(.+)@(.+)/
509
509
  username = _[1] # "user"
@@ -511,7 +511,7 @@ if email =~ /(.+)@(.+)/
511
511
  ```
512
512
 
513
513
  **Phone number parsing:**
514
- ```rip
514
+ ```coffee
515
515
  phone = "2125551234"
516
516
  if phone =~ /^([2-9]\d\d)([2-9]\d\d)(\d{4})$/
517
517
  formatted = "(#{_[1]}) #{_[2]}-#{_[3]}"
@@ -522,7 +522,7 @@ if phone =~ /^([2-9]\d\d)([2-9]\d\d)(\d{4})$/
522
522
 
523
523
  ### Syntax
524
524
 
525
- ```rip
525
+ ```coffee
526
526
  value[/pattern/] # Returns full match (capture 0)
527
527
  value[/pattern/, n] # Returns capture group n
528
528
  ```
@@ -530,17 +530,17 @@ value[/pattern/, n] # Returns capture group n
530
530
  ### Examples
531
531
 
532
532
  **Simple match:**
533
- ```rip
533
+ ```coffee
534
534
  "steve"[/eve/] # Returns "eve"
535
535
  ```
536
536
 
537
537
  **Capture group:**
538
- ```rip
538
+ ```coffee
539
539
  "steve"[/e(v)e/, 1] # Returns "v"
540
540
  ```
541
541
 
542
542
  **Email domain:**
543
- ```rip
543
+ ```coffee
544
544
  domain = "user@example.com"[/@(.+)$/, 1]
545
545
  # Returns: "example.com"
546
546
  ```
@@ -549,7 +549,7 @@ domain = "user@example.com"[/@(.+)$/, 1]
549
549
 
550
550
  The real power comes from using both features together:
551
551
 
552
- ```rip
552
+ ```coffee
553
553
  # Parse, validate, and format in clean steps
554
554
  email = "Admin@Company.COM"
555
555
  if email =~ /^([^@]+)@([^@]+)$/i
@@ -562,7 +562,7 @@ if email =~ /^([^@]+)@([^@]+)$/i
562
562
 
563
563
  One of the most powerful use cases is building validators:
564
564
 
565
- ```rip
565
+ ```coffee
566
566
  validators =
567
567
  # Extract and validate in one expression
568
568
  id: (v) -> v[/^([1-9]\d{0,19})$/] and parseInt(_[1])
@@ -589,7 +589,7 @@ validators =
589
589
 
590
590
  Rip supports heregexes - extended regular expressions that allow whitespace and comments for readability:
591
591
 
592
- ```rip
592
+ ```coffee
593
593
  pattern = ///
594
594
  ^ \d+ # starts with digits
595
595
  \s* # optional whitespace
@@ -607,7 +607,7 @@ pattern = ///
607
607
 
608
608
  By default, **rejects strings with newlines**:
609
609
 
610
- ```rip
610
+ ```coffee
611
611
  # Safe - rejects malicious input
612
612
  userInput = "test\nmalicious"
613
613
  userInput =~ /^test$/ # Returns null! (newline detected)
@@ -632,5 +632,5 @@ text =~ /line2/m # Works! (/m flag allows newlines)
632
632
 
633
633
  **See Also:**
634
634
  - [INTERNALS.md](INTERNALS.md) - Compiler and parser details
635
- - [PHILOSOPHY.md](PHILOSOPHY.md) - Why Rip exists
635
+ - [RATIONALE.md](RATIONALE.md) - Why Rip exists
636
636
  - [BROWSER.md](BROWSER.md) - Browser usage and REPL guide
package/docs/INTERNALS.md CHANGED
@@ -43,7 +43,7 @@ Source Code → CoffeeScript Lexer → Solar Parser → S-Expressions → Codege
43
43
 
44
44
  ## Example Flow
45
45
 
46
- ```rip
46
+ ```coffee
47
47
  # Input
48
48
  x = 42
49
49
 
@@ -320,7 +320,7 @@ generate(sexpr, context = 'statement') {
320
320
 
321
321
  **Example: Comprehensions**
322
322
 
323
- ```rip
323
+ ```coffee
324
324
  # Statement context (result discarded) → Plain loop
325
325
  console.log x for x in arr
326
326
  # → for (const x of arr) { console.log(x); }
@@ -364,7 +364,7 @@ fn = function() {
364
364
 
365
365
  **Functions automatically become async or generators:**
366
366
 
367
- ```rip
367
+ ```coffee
368
368
  # Contains await → becomes async function
369
369
  getData = ->
370
370
  result = await fetch(url)
@@ -405,7 +405,7 @@ fetchAll = ->
405
405
  | `a ??= 10` | ES6 | `a ??= 10` |
406
406
 
407
407
  **Mix and match:**
408
- ```rip
408
+ ```coffee
409
409
  obj?.arr?[0] # ES6 + CoffeeScript together!
410
410
  ```
411
411
 
@@ -413,7 +413,7 @@ obj?.arr?[0] # ES6 + CoffeeScript together!
413
413
 
414
414
  **Traditional for loops instead of wasteful IIFEs:**
415
415
 
416
- ```rip
416
+ ```coffee
417
417
  # Optimized to traditional loop
418
418
  for i in [1...100]
419
419
  process(i)
@@ -424,7 +424,7 @@ for i in [1...100]
424
424
  ```
425
425
 
426
426
  **Reverse iteration support:**
427
- ```rip
427
+ ```coffee
428
428
  for i in [10..1] by -1
429
429
  process(i)
430
430
  # → for (let i = 10; i >= 1; i--) { process(i); }
@@ -472,7 +472,7 @@ Comprehensions can act as **data builders** or **control loops**. Rip distinguis
472
472
  - **Benefit:** Automatic optimization - no wasteful array building!
473
473
 
474
474
  **Example of improvement:**
475
- ```rip
475
+ ```coffee
476
476
  fn = ->
477
477
  process x for x in arr # ← Rip: plain loop! CS: IIFE (wasteful)
478
478
  doMore()
@@ -540,7 +540,7 @@ for (const x of arr) {
540
540
 
541
541
  ### Own + Guard + Value Variable (Critical!)
542
542
 
543
- ```rip
543
+ ```coffee
544
544
  for own k, v of obj when v > 5
545
545
  process(k, v)
546
546
  ```
@@ -562,13 +562,13 @@ for (const k in obj) {
562
562
  ### Async Comprehensions
563
563
 
564
564
  **Sequential (await in body):**
565
- ```rip
565
+ ```coffee
566
566
  # IIFE is async, awaits happen serially inside loop
567
567
  results = (await fetchData(url) for url in urls)
568
568
  ```
569
569
 
570
570
  **Parallel (recommended for I/O):**
571
- ```rip
571
+ ```coffee
572
572
  # Build array of promises, then await all in parallel
573
573
  results = await Promise.all (fetchData(url) for url in urls)
574
574
  ```
@@ -853,5 +853,5 @@ Solar's s-expression mode is the **secret sauce** that makes Rip practical:
853
853
 
854
854
  **See Also:**
855
855
  - [GUIDE.md](GUIDE.md) - Language features and syntax
856
- - [PHILOSOPHY.md](PHILOSOPHY.md) - Design decisions and rationale
856
+ - [RATIONALE.md](RATIONALE.md) - Design decisions and rationale
857
857
  - [BROWSER.md](BROWSER.md) - Browser usage and REPL guide
@@ -0,0 +1,180 @@
1
+ <p><img src="rip.svg" alt="Rip Logo" width="100"></p>
2
+
3
+ # Why Rip Exists
4
+
5
+ > Philosophy, design decisions, and the case for a modern CoffeeScript successor.
6
+
7
+ ---
8
+
9
+ ## The Short Version
10
+
11
+ Rip exists because:
12
+
13
+ 1. **Simplicity scales** — S-expressions make compilers 50% smaller and 10x easier to maintain
14
+ 2. **Zero dependencies** — True autonomy from the npm ecosystem
15
+ 3. **Modern output** — ES2022 everywhere, no legacy baggage
16
+ 4. **Unique features** — 10+ innovations CoffeeScript never had
17
+ 5. **Reactivity as operators** — `:=`, `~=`, `~>` are language syntax, not library imports
18
+ 6. **Self-hosting** — Rip compiles itself, including its own parser generator
19
+
20
+ ---
21
+
22
+ ## 1. Why S-Expressions
23
+
24
+ Most compilers use complex AST node classes. Rip uses **simple arrays**:
25
+
26
+ ```javascript
27
+ // Traditional AST (CoffeeScript, TypeScript, Babel)
28
+ class BinaryOp {
29
+ constructor(op, left, right) { ... }
30
+ compile() { /* 50+ lines */ }
31
+ }
32
+
33
+ // Rip's S-Expression
34
+ ["+", left, right] // That's it!
35
+ ```
36
+
37
+ **Result:** CoffeeScript's compiler is 17,760 LOC. Rip's is ~11,000 LOC — smaller, yet includes a complete reactive runtime.
38
+
39
+ ### The Fundamental Rule
40
+
41
+ > **Transform the IR (s-expressions), not the output (strings)**
42
+
43
+ This single principle eliminates entire categories of bugs. When your IR is simple data (arrays), transformations are trivial and debuggable:
44
+
45
+ ```javascript
46
+ // Debugging: inspect the data directly
47
+ console.log(sexpr);
48
+ // ["comprehension", ["*", "x", 2], [["for-in", ["x"], ["array", 1, 2, 3]]], []]
49
+ // Clear structure, 2-3 minutes to debug
50
+
51
+ // vs. string manipulation
52
+ console.log(code);
53
+ // "(() => {\n const result = [];\n for (const x of arr) {\n..."
54
+ // Blob of text, 20-30 minutes to debug
55
+ ```
56
+
57
+ **Lisp got this right 60 years ago.** Code is data, data is code.
58
+
59
+ ---
60
+
61
+ ## 2. Rip vs CoffeeScript
62
+
63
+ | Metric | CoffeeScript | Rip |
64
+ |--------|--------------|-----|
65
+ | **Feature Parity** | Baseline | ~99% |
66
+ | **Unique Features** | 0 | 10+ innovations |
67
+ | **Dependencies** | Multiple | **ZERO** |
68
+ | **Self-Hosting** | No | **YES** |
69
+ | **Total LOC** | 17,760 | ~11,000 |
70
+ | **Output** | ES6 (2017) | ES2022 |
71
+ | **Reactivity** | None | Built-in |
72
+
73
+ ### What Rip Adds
74
+
75
+ 1. **Reactivity as operators** — `:=` state, `~=` computed, `~>` effects
76
+ 2. **Ternary operator** — `x ? a : b` (freed by using `??` for nullish)
77
+ 3. **Dammit operator** — `fetchData!` calls AND awaits
78
+ 4. **Ruby-style regex** — `str =~ /pattern/` with captures in `_`
79
+ 5. **Dual optional syntax** — Both CoffeeScript soak AND ES6 optional chaining
80
+ 6. **Void functions** — `def process!` suppresses implicit returns
81
+ 7. **Smart comprehensions** — Context-aware (loop vs. array building)
82
+ 8. **`__DATA__` marker** — Ruby-style inline data sections
83
+ 9. **Heregex** — Extended regex with comments and whitespace
84
+ 10. **Zero dependencies** — Everything included, even the parser generator
85
+
86
+ ### The Complete Feature Table
87
+
88
+ | Feature | CoffeeScript | Rip | Winner |
89
+ |---------|-------------|------|--------|
90
+ | Optional operators | 4 soak | 10 (5 soak + 5 ES6) | Rip |
91
+ | Ternary | No | Yes | Rip |
92
+ | Regex features | Basic | Ruby-style (`=~`, indexing) | Rip |
93
+ | Async shorthand | No | Dammit operator (`!`) | Rip |
94
+ | Void functions | No | `def fn!` | Rip |
95
+ | Reactivity | None | `:=`, `~=`, `~>` | Rip |
96
+ | Comprehension optimization | Always IIFE | Context-aware | Rip |
97
+ | Modules | CommonJS | ES6 | Rip |
98
+ | Classes | ES5 | ES6 | Rip |
99
+ | Dependencies | Multiple | **ZERO** | Rip |
100
+ | Parser generator | External (Jison) | **Built-in (solar.rip)** | Rip |
101
+ | Self-hosting | No | **Yes** | Rip |
102
+
103
+ ---
104
+
105
+ ## 3. The Case Against CoffeeScript (And Why Rip Is Different)
106
+
107
+ The strongest argument against CoffeeScript in 2026:
108
+
109
+ - **Ecosystem abandoned** — Hiring, tooling, community all gone
110
+ - **TypeScript won** — Type safety became the standard
111
+ - **JavaScript absorbed it** — `?.`, `??`, `=>`, classes, destructuring all native now
112
+ - **Tooling rotted** — IDE support deprecated, build plugins unmaintained
113
+
114
+ **These are valid criticisms.** Rip addresses every one:
115
+
116
+ | Criticism | CoffeeScript's Problem | Rip's Answer |
117
+ |-----------|----------------------|--------------|
118
+ | No ecosystem | Dead community | Zero dependencies — doesn't need one |
119
+ | No types | Can't integrate with TS tooling | ES2022 output works with any JS toolchain |
120
+ | Tooling rot | Plugins unmaintained | Self-contained — nothing to maintain |
121
+ | No innovation | Frozen since 2017 | 10+ unique features, active development |
122
+ | No corporate backing | Abandoned | Self-hosting — depends on nothing |
123
+
124
+ **The key insight:** CoffeeScript failed because it depended on an ecosystem that moved on. Rip succeeds by depending on nothing.
125
+
126
+ ---
127
+
128
+ ## 4. Reactivity as a Language Feature
129
+
130
+ This is Rip's signature innovation. While React, Vue, and Solid provide reactivity through library imports, Rip provides it as **language operators**:
131
+
132
+ ```coffee
133
+ count := 0 # State — reactive container
134
+ doubled ~= count * 2 # Computed — auto-updates when count changes
135
+ ~> console.log count # Effect — runs when dependencies change
136
+ ```
137
+
138
+ Compare to React:
139
+ ```javascript
140
+ import { useState, useMemo, useEffect } from 'react';
141
+ const [count, setCount] = useState(0);
142
+ const doubled = useMemo(() => count * 2, [count]);
143
+ useEffect(() => { console.log(count); }, [count]);
144
+ ```
145
+
146
+ **No imports. No hooks. No dependency arrays. Just operators.**
147
+
148
+ The reactive runtime is ~200 lines, embedded in the compiler, and only included when reactive operators are used.
149
+
150
+ ---
151
+
152
+ ## 5. Design Principles
153
+
154
+ ### Simplicity Scales
155
+
156
+ Simple IR (s-expressions), clear pipeline (lex → parse → generate), minimal code, comprehensive tests. Every design decision optimizes for **long-term maintainability**, not short-term cleverness.
157
+
158
+ ### Zero Dependencies Is a Feature
159
+
160
+ ```json
161
+ { "dependencies": {} }
162
+ ```
163
+
164
+ Everything included: compiler, parser generator, REPL, browser bundle, test framework. No supply chain attacks. No version conflicts. No `node_modules` bloat.
165
+
166
+ ### Self-Hosting Proves Quality
167
+
168
+ Rip compiles its own parser generator (`solar.rip` → `parser.js`). If the compiler can compile itself, it works.
169
+
170
+ ---
171
+
172
+ **See Also:**
173
+ - [GUIDE.md](GUIDE.md) — Complete language reference
174
+ - [REACTIVITY.md](REACTIVITY.md) — Reactivity deep dive
175
+ - [INTERNALS.md](INTERNALS.md) — Compiler architecture
176
+ - [BROWSER.md](BROWSER.md) — Browser usage and REPL
177
+
178
+ ---
179
+
180
+ *Version 2.9.0 — 1115/1115 tests passing — Zero dependencies — Self-hosting*
@@ -51,7 +51,7 @@ Every reactive system reduces to these three concepts:
51
51
 
52
52
  ## Quick Example
53
53
 
54
- ```rip
54
+ ```coffee
55
55
  count := 0 # count has state 0
56
56
  doubled ~= count * 2 # doubled always equals count * 2
57
57
  logger ~> console.log count # reacts to count changes
@@ -71,7 +71,7 @@ increment() # Logs: 2
71
71
 
72
72
  State creates a **reactive container** that tracks its readers and notifies them on change.
73
73
 
74
- ```rip
74
+ ```coffee
75
75
  count := 0 # count has state 0
76
76
  count += 1 # Update triggers dependents
77
77
  ```
@@ -91,7 +91,7 @@ count.value += 1;
91
91
 
92
92
  Computed creates a **computed value** that automatically updates when dependencies change.
93
93
 
94
- ```rip
94
+ ```coffee
95
95
  count := 0
96
96
  doubled ~= count * 2 # doubled always equals count * 2
97
97
  ```
@@ -105,7 +105,7 @@ doubled ~= count * 2 # doubled always equals count * 2
105
105
 
106
106
  The effect operator runs **side effects** when dependencies change. Dependencies are auto-tracked from reactive values read in the body.
107
107
 
108
- ```rip
108
+ ```coffee
109
109
  ~> document.title = "Count: #{count}"
110
110
  ```
111
111
 
@@ -115,7 +115,7 @@ The effect operator runs **side effects** when dependencies change. Dependencies
115
115
  - **Controllable** — optionally assign to a variable to control the effect
116
116
 
117
117
  **Syntax:**
118
- ```rip
118
+ ```coffee
119
119
  # Fire and forget (no assignment)
120
120
  ~> console.log count
121
121