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.
@@ -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*