rip-lang 3.6.1 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,7 +7,33 @@ 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
- ## [3.6.0] - 2026-02-10
10
+ ## [3.7.0] - 2026-02-11
11
+
12
+ ### Pipe Operator (`|>`)
13
+
14
+ - **First-arg insertion pipe** — `x |> fn` compiles to `fn(x)`, and `x |> fn(y)` compiles to `fn(x, y)`. Combines the simplicity of F#-style pipes with multi-arg support like Elixir — no placeholder syntax needed. Left-associative, chains naturally: `5 |> double |> add(1)`.
15
+ - Works with dotted references (`x |> Math.sqrt`, `x |> console.log`) and dotted calls (`x |> obj.method(y)` → `obj.method(x, y)`).
16
+ - Implicit calls close at pipe boundaries — `x |> fn 1 |> bar 2` works correctly without parens.
17
+
18
+ ### Method Assignment (`.=`) — A Rip Original
19
+
20
+ - **Compound method assignment** — `x .= trim()` compiles to `x = x.trim()`. The method-call equivalent of `+=`. No other language has this. Combined with implicit `it`, enables concise transformation pipelines: `items .= filter -> it.active` then `items .= map -> it.name`.
21
+
22
+ ### Prototype Operator (`::`)
23
+
24
+ - **CoffeeScript-style prototype access restored** — `String::trim` compiles to `String.prototype.trim`. Disambiguated from type annotations by spacing: `::` with no space is prototype, `::` with a space is a type annotation. Both coexist cleanly.
25
+
26
+ ### `loop n` — Repeat N Times
27
+
28
+ - **Counted loop** — `loop 5 -> body` compiles to `for (let _i = 0; _i < 5; _i++) { body }`. Works with literals, variables, and expressions.
29
+
30
+ ### Implicit `it` Parameter
31
+
32
+ - **Auto-injected parameter** — Arrow functions with no explicit params that reference `it` in the body automatically inject `it` as the parameter. `arr.filter -> it > 5` compiles to `arr.filter(function(it) { return (it > 5); })`. Works with both `->` and `=>`. Stops at nested function boundaries.
33
+
34
+ ### Negative Indexing
35
+
36
+ - **Python-style negative indexing** — `arr[-1]` compiles to `arr.at(-1)`, `arr[-2]` to `arr.at(-2)`, etc. Works on arrays and strings. Optional variant: `arr?[-1]` → `arr?.at(-1)`. Only literal negative numbers trigger the transform; variable indexes pass through unchanged.
11
37
 
12
38
  ### Browser Runtime — Full Async/Await Support
13
39
 
package/README.md CHANGED
@@ -9,15 +9,15 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-3.6.0-blue.svg" alt="Version"></a>
12
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-3.7.0-blue.svg" alt="Version"></a>
13
13
  <a href="#zero-dependencies"><img src="https://img.shields.io/badge/dependencies-ZERO-brightgreen.svg" alt="Dependencies"></a>
14
- <a href="#"><img src="https://img.shields.io/badge/tests-1140%2F1140-brightgreen.svg" alt="Tests"></a>
14
+ <a href="#"><img src="https://img.shields.io/badge/tests-1219%2F1219-brightgreen.svg" alt="Tests"></a>
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
16
16
  </p>
17
17
 
18
18
  ---
19
19
 
20
- Rip is a modern language inspired by CoffeeScript. It compiles to **ES2022** (classes, `?.`, `??`, modules), adds about a **dozen new operators**, includes **built-in reactivity**, and sports a self-hosting compiler with **zero dependencies** — all in about 10,300 lines of code.
20
+ Rip is a modern language inspired by CoffeeScript. It compiles to **ES2022** (classes, `?.`, `??`, modules), adds about a **dozen new operators**, includes **built-in reactivity**, and sports a self-hosting compiler with **zero dependencies** — all in about 10,800 lines of code.
21
21
 
22
22
  > **No imports. No hooks. No dependency arrays. Just write code.**
23
23
 
@@ -38,7 +38,7 @@ get '/users/:id' -> # RESTful API endpoint, comma-less
38
38
 
39
39
  **What makes Rip different:**
40
40
  - **Modern output** — ES2022 with native classes, `?.`, `??`, modules
41
- - **New operators** — `!`, `!?`, `//`, `%%`, `=~`, `.new()`, and more
41
+ - **New operators** — `!`, `!?`, `//`, `%%`, `=~`, `|>`, `.new()`, and more
42
42
  - **Reactive operators** — `:=`, `~=`, `~>` as language syntax
43
43
  - **Optional types** — `::` annotations, `::=` aliases, `.d.ts` emission
44
44
  - **Zero dependencies** — everything included, even the parser generator
@@ -122,6 +122,21 @@ for x as iterable # ES6 for-of on any iterable
122
122
 
123
123
  for x as! asyncIterable # Async iteration shorthand
124
124
  console.log x # Equivalent to: for await x as asyncIterable
125
+
126
+ loop # Infinite loop (while true)
127
+ process!
128
+ loop 5 # Repeat N times
129
+ console.log "hi"
130
+ ```
131
+
132
+ ### Implicit `it`
133
+
134
+ Arrow functions with no params that reference `it` auto-inject it as the parameter:
135
+
136
+ ```coffee
137
+ users.filter -> it.active # → users.filter(function(it) { ... })
138
+ names = users.map -> it.name # no need to name a throwaway variable
139
+ orders.filter -> it.total > 100 # works with any expression
125
140
  ```
126
141
 
127
142
  ### Reactivity
@@ -168,6 +183,7 @@ Compiles to `.js` (types erased) + `.d.ts` (types preserved) — full IDE suppor
168
183
  | `!?` (otherwise) | `val !? 5` | Default only if `undefined` |
169
184
  | `?` (existence) | `x?` | True if `x != null` |
170
185
  | `?:` (ternary) | `x > 0 ? 'yes' : 'no'` | JS-style ternary expression |
186
+ | `if...else` (postfix) | `"yes" if cond else "no"` | Python-style ternary expression |
171
187
  | `?.` `?.[]` `?.()` | `a?.b` `a?.[0]` `a?.()` | Optional chaining (ES6) |
172
188
  | `?[]` `?()` | `a?[0]` `a?(x)` | Optional chaining shorthand |
173
189
  | `??` | `a ?? b` | Nullish coalescing |
@@ -177,6 +193,15 @@ Compiles to `.js` (types erased) + `.d.ts` (types preserved) — full IDE suppor
177
193
  | `=~` | `str =~ /Hello, (\w+)/` | Match (captures in `_`) |
178
194
  | `[//, n]` | `str[/Hello, (\w+)/, 1]` | Extract capture n |
179
195
  | `.new()` | `Dog.new()` | Ruby-style constructor |
196
+ | `::` (prototype) | `String::trim` | `String.prototype.trim` |
197
+ | `[-n]` (negative index) | `arr[-1]` | Last element via `.at()` |
198
+ | `*` (string repeat) | `"-" * 40` | String repeat via `.repeat()` |
199
+ | `<` `<=` (chained) | `1 < x < 10` | Chained comparisons |
200
+ | `\|>` (pipe) | `x \|> fn` or `x \|> fn(y)` | Pipe operator (first-arg insertion) |
201
+ | `not in` | `x not in arr` | Negated membership test |
202
+ | `not of` | `k not of obj` | Negated key existence |
203
+ | `.=` (method assign) | `x .= trim()` | `x = x.trim()` — compound method assignment |
204
+ | `*` (merge assign) | `*obj = {a: 1}` | `Object.assign(obj, {a: 1})` |
180
205
  | `or return` | `x = get() or return err` | Guard clause (Ruby-style) |
181
206
  | `?? throw` | `x = get() ?? throw err` | Nullish guard |
182
207
 
@@ -254,9 +279,9 @@ See [@rip-lang/ui](packages/ui/) for the full framework: file-based router, reac
254
279
  | **Reactivity** | None | Built-in |
255
280
  | **Dependencies** | Multiple | Zero |
256
281
  | **Self-hosting** | No | Yes |
257
- | **Lexer** | 3,558 LOC | 1,867 LOC |
258
- | **Compiler** | 10,346 LOC | 3,292 LOC |
259
- | **Total** | 17,760 LOC | ~10,300 LOC |
282
+ | **Lexer** | 3,558 LOC | 1,958 LOC |
283
+ | **Compiler** | 10,346 LOC | 3,378 LOC |
284
+ | **Total** | 17,760 LOC | ~10,800 LOC |
260
285
 
261
286
  Smaller codebase, modern output, built-in reactivity.
262
287
 
@@ -291,25 +316,25 @@ await rip("res = fetch! 'https://api.example.com/todos/1'; res.json!") // → {
291
316
 
292
317
  ```
293
318
  Source -> Lexer -> emitTypes -> Parser -> S-Expressions -> Codegen -> JavaScript
294
- (1,867) (types.js) (357) ["=", "x", 42] (3,292) + source map
319
+ (1,958) (types.js) (359) ["=", "x", 42] (3,378) + source map
295
320
  ```
296
321
 
297
322
  Simple arrays (with `.loc`) instead of AST node classes. The compiler is self-hosting — `bun run parser` rebuilds from source.
298
323
 
299
324
  | Component | File | Lines |
300
325
  |-----------|------|-------|
301
- | Lexer + Rewriter | `src/lexer.js` | 1,867 |
302
- | Compiler + Codegen | `src/compiler.js` | 3,292 |
303
- | Type System | `src/types.js` | 719 |
326
+ | Lexer + Rewriter | `src/lexer.js` | 1,958 |
327
+ | Compiler + Codegen | `src/compiler.js` | 3,378 |
328
+ | Type System | `src/types.js` | 1,099 |
304
329
  | Component System | `src/components.js` | 1,240 |
305
330
  | Source Maps | `src/sourcemaps.js` | 122 |
306
- | Parser (generated) | `src/parser.js` | 357 |
307
- | Grammar | `src/grammar/grammar.rip` | 935 |
331
+ | Parser (generated) | `src/parser.js` | 359 |
332
+ | Grammar | `src/grammar/grammar.rip` | 945 |
308
333
  | Parser Generator | `src/grammar/solar.rip` | 916 |
309
- | REPL | `src/repl.js` | 707 |
334
+ | REPL | `src/repl.js` | 582 |
310
335
  | Browser Entry | `src/browser.js` | 119 |
311
336
  | Tags | `src/tags.js` | 63 |
312
- | **Total** | | **~10,325** |
337
+ | **Total** | | **~10,774** |
313
338
 
314
339
  ---
315
340
 
@@ -319,7 +344,7 @@ Rip includes optional packages for full-stack development:
319
344
 
320
345
  | Package | Version | Purpose |
321
346
  |---------|---------|---------|
322
- | [rip-lang](https://www.npmjs.com/package/rip-lang) | 3.6.0 | Core language compiler |
347
+ | [rip-lang](https://www.npmjs.com/package/rip-lang) | 3.7.0 | Core language compiler |
323
348
  | [@rip-lang/api](packages/api/) | 1.1.4 | HTTP framework (Sinatra-style routing, 37 validators) |
324
349
  | [@rip-lang/server](packages/server/) | 1.1.3 | Multi-worker app server (hot reload, HTTPS, mDNS) |
325
350
  | [@rip-lang/db](packages/db/) | 1.1.2 | DuckDB server with official UI (pure Bun FFI) |
@@ -365,7 +390,7 @@ rip file.rip # Run
365
390
  rip -c file.rip # Compile
366
391
  rip -t file.rip # Tokens
367
392
  rip -s file.rip # S-expressions
368
- bun run test # 1140 tests
393
+ bun run test # 1219 tests
369
394
  bun run parser # Rebuild parser
370
395
  bun run browser # Build browser bundle
371
396
  ```
package/docs/NOTES.md ADDED
@@ -0,0 +1,93 @@
1
+ <img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.png" style="width:50px" /> <br>
2
+
3
+ # Rip — Notes
4
+
5
+ Ideas, future plans, and design thoughts for Rip.
6
+
7
+ ---
8
+
9
+ ## Standard Library (`stdlib`)
10
+
11
+ Rip is a zero-dependency language, but a small standard library of useful
12
+ utilities would save users from writing the same one-liners in every project.
13
+ These are **not** language features — they're plain functions that could ship
14
+ as a prelude or optional import.
15
+
16
+ ### Candidates
17
+
18
+ ```coffee
19
+ # Printing (Ruby's p)
20
+ p = console.log
21
+
22
+ # Exit with optional code (uses implicit `it`)
23
+ exit = -> process.exit(it)
24
+
25
+ # Tap — call a function for side effects, return the original value
26
+ # Useful in pipe chains: data |> tap(console.log) |> process
27
+ tap = (v, fn) -> fn(v); v
28
+
29
+ # Identity — returns its argument unchanged
30
+ # Useful as a default callback: items.filter(id)
31
+ id = -> it
32
+
33
+ # No-op — does nothing
34
+ # Useful as a default handler: onClick ?= noop
35
+ noop = ->
36
+
37
+ # String method aliases (shorter names for common checks)
38
+ String::starts = String::startsWith
39
+ String::ends = String::endsWith
40
+ String::has = String::includes
41
+
42
+ # Clamp a value to a range
43
+ clamp = (v, lo, hi) -> Math.min(Math.max(v, lo), hi)
44
+
45
+ # Sleep for N milliseconds (returns a Promise)
46
+ sleep = (ms) -> new Promise (resolve) -> setTimeout resolve, ms
47
+
48
+ # Times helper — call a function N times, collect results
49
+ times = (n, fn) -> (fn(i) for i in [0...n])
50
+ ```
51
+
52
+ ### Design Questions
53
+
54
+ - **Prelude vs import?** Should these be injected automatically (like Go's
55
+ `fmt` or Rip's reactive runtime), or explicitly imported (`import { p, tap }
56
+ from '@rip-lang/std'`)? Leaning toward explicit — Rip's philosophy is zero
57
+ magic in the output.
58
+
59
+ - **Scope?** Keep it tiny. A stdlib that grows to 500 functions defeats the
60
+ purpose. Each entry should save real keystrokes on something people do
61
+ constantly.
62
+
63
+ - **Node vs Browser?** Some helpers (like `exit`) are Node-only. Others (like
64
+ `p`, `tap`, `sleep`) work everywhere. May want to split into `std` (universal)
65
+ and `std/node` (server-only).
66
+
67
+ ---
68
+
69
+ ## Future Syntax Ideas
70
+
71
+ Ideas that have been discussed but not yet implemented. Each would need
72
+ design discussion before building.
73
+
74
+ - **`defer`** — Go-style cleanup that runs when the function exits. Compiles
75
+ to try/finally. `defer file.close()`.
76
+
77
+ - **`is a` / `isnt a`** — Readable instanceof. `x is a String` →
78
+ `x instanceof String`.
79
+
80
+ - **`.starts?` / `.ends?` / `.has?`** — Ruby-style question-mark methods.
81
+ `url.starts? "https"` → `url.startsWith("https")`.
82
+
83
+ - **Pattern matching** — `match value` with destructuring arms. Big feature,
84
+ needs careful design.
85
+
86
+ - **Reactive resource operator (`~>?`)** — Language-level `createResource`.
87
+ `user ~>? fetch!("/api/users/#{id}").json!` gives `user.loading`,
88
+ `user.error`, `user.data`. Park until real-world usage shows demand.
89
+
90
+ - **Pipe operator (`|>`) — Hack-style placeholder** — Currently Rip uses
91
+ Elixir-style first-arg insertion. A `%` placeholder for arbitrary position
92
+ (`data |> fn(1, %, 3)`) could be added later if needed. Current design
93
+ covers 95%+ of cases.
package/docs/RIP-GUIDE.md CHANGED
@@ -117,6 +117,7 @@ else
117
117
 
118
118
  # Ternary
119
119
  status = active ? "on" : "off"
120
+ label = "big" if x > 5 else "small"
120
121
 
121
122
  # Postfix
122
123
  console.log "hi" if ready
@@ -147,11 +148,26 @@ for key, value of object
147
148
  for i in [1..10] # inclusive (1 to 10)
148
149
  for i in [1...10] # exclusive (1 to 9)
149
150
 
151
+ # Loop N times
152
+ loop 5
153
+ console.log "hi"
154
+
150
155
  # Comprehensions
151
156
  squares = (x * x for x in [1..10])
152
157
  evens = (x for x in items when x % 2 is 0)
153
158
  ```
154
159
 
160
+ ### Implicit `it`
161
+
162
+ Arrow functions with no params that reference `it` auto-inject it as the parameter:
163
+
164
+ ```coffee
165
+ users.filter -> it.active # instead of (u) -> u.active
166
+ names = users.map -> it.name # instead of (u) -> u.name
167
+ orders.filter -> it.total > 100 # works with any expression
168
+ items.map => it.toUpperCase() # works with fat arrows too
169
+ ```
170
+
155
171
  ---
156
172
 
157
173
  ## Operators
@@ -174,6 +190,15 @@ Rip extends JavaScript with powerful operators:
174
190
  | `:=` | State | `count := 0` | Reactive signal |
175
191
  | `~=` | Computed | `doubled ~= x * 2` | Reactive computed |
176
192
  | `~>` | Effect | `~> console.log x` | Reactive side effect |
193
+ | `::` | Prototype | `String::trim` | `String.prototype.trim` |
194
+ | `[-n]` | Negative index | `arr[-1]` | `arr.at(-1)` |
195
+ | `*` | String repeat | `"-" * 40` | `"-".repeat(40)` |
196
+ | `<` `<=` | Chained | `1 < x < 10` | `(1 < x) && (x < 10)` |
197
+ | `.=` | Method assign | `x .= trim()` | `x = x.trim()` |
198
+ | `*` | Merge assign | `*obj = {a: 1}` | `Object.assign(obj, {a: 1})` |
199
+ | `not in` | Not in | `x not in arr` | Negated membership |
200
+ | `not of` | Not of | `k not of obj` | Negated key existence |
201
+ | `if...else` | Postfix ternary | `"a" if x else "b"` | `x ? "a" : "b"` |
177
202
  | `**` | Power | `2 ** 10` | `1024` |
178
203
  | `..` | Range | `[1..5]` | Inclusive range |
179
204
  | `...` | Spread/rest | `[...a, ...b]` | ES6 spread |
@@ -211,6 +236,19 @@ if text =~ /Hello, (\w+)/
211
236
  domain = "user@example.com"[/@(.+)$/, 1] # "example.com"
212
237
  ```
213
238
 
239
+ ### Pipe Operator (`|>`)
240
+
241
+ Pipes a value into a function as the first argument. Chains left-to-right:
242
+
243
+ ```coffee
244
+ 5 |> double # → double(5)
245
+ 5 |> add(3) # → add(5, 3)
246
+ 5 |> double |> add(1) # → add(double(5), 1)
247
+ data |> JSON.stringify(null, 2) # → JSON.stringify(data, null, 2)
248
+ ```
249
+
250
+ If the right side is a bare reference, it's called with the piped value. If the right side is already a call, the piped value is inserted as the first argument. This is the Elixir-style pipe — cleaner than F# (multi-arg works) and simpler than Hack (no placeholder needed).
251
+
214
252
  ### Guard Clauses
215
253
 
216
254
  ```coffee
@@ -633,7 +671,7 @@ rip -d file.rip # Generate .d.ts
633
671
  rip -t file.rip # Show tokens
634
672
  rip -s file.rip # Show S-expressions
635
673
  rip -q -c file.rip # Quiet (no headers)
636
- bun run test # Run test suite (1,140 tests)
674
+ bun run test # Run test suite
637
675
  ```
638
676
 
639
677
  ---
@@ -45,7 +45,7 @@ class BinaryOp {
45
45
  ["+", left, right] // That's it!
46
46
  ```
47
47
 
48
- **Result:** CoffeeScript's compiler is 17,760 LOC. Rip's is ~10,300 LOC — smaller, yet includes a complete reactive runtime, type system, component system, and source maps.
48
+ **Result:** CoffeeScript's compiler is 17,760 LOC. Rip's is ~10,800 LOC — smaller, yet includes a complete reactive runtime, type system, component system, and source maps.
49
49
 
50
50
  > **Transform the IR (s-expressions), not the output (strings).**
51
51
 
@@ -66,7 +66,7 @@ console.log(code);
66
66
  | Feature | CoffeeScript | Rip |
67
67
  |---------|-------------|------|
68
68
  | Optional chaining | 4 soak operators | ES6 `?.` / `?.[]` / `?.()` + shorthand `?[]` / `?()` |
69
- | Ternary | No | `x ? a : b` |
69
+ | Ternary | No | `x ? a : b` and `a if x else b` |
70
70
  | Regex features | Basic | Ruby-style (`=~`, indexing, captures in `_`) |
71
71
  | Async shorthand | No | Dammit operator (`!`) |
72
72
  | Void functions | No | `def fn!` |
@@ -77,7 +77,7 @@ console.log(code);
77
77
  | Dependencies | Multiple | **Zero** |
78
78
  | Parser generator | External (Jison) | **Built-in (Solar)** |
79
79
  | Self-hosting | No | **Yes** |
80
- | Total LOC | 17,760 | ~10,300 |
80
+ | Total LOC | 17,760 | ~10,800 |
81
81
 
82
82
  ## Design Principles
83
83
 
@@ -93,7 +93,7 @@ console.log(code);
93
93
 
94
94
  ```
95
95
  Source Code → Lexer → emitTypes → Parser → S-Expressions → Codegen → JavaScript
96
- (1,867) (types.js) (357) (arrays + .loc) (3,292) + source map
96
+ (1,958) (types.js) (359) (arrays + .loc) (3,378) + source map
97
97
 
98
98
  file.d.ts (when types: "emit")
99
99
  ```
@@ -102,14 +102,14 @@ Source Code → Lexer → emitTypes → Parser → S-Expressions → C
102
102
 
103
103
  | File | Purpose | Lines | Modify? |
104
104
  |------|---------|-------|---------|
105
- | `src/lexer.js` | Lexer + Rewriter | 1,867 | Yes |
106
- | `src/compiler.js` | Compiler + Code Generator | 3,292 | Yes |
107
- | `src/types.js` | Type System (lexer sidecar) | 719 | Yes |
105
+ | `src/lexer.js` | Lexer + Rewriter | 1,958 | Yes |
106
+ | `src/compiler.js` | Compiler + Code Generator | 3,378 | Yes |
107
+ | `src/types.js` | Type System (lexer sidecar) | 1,099 | Yes |
108
108
  | `src/components.js` | Component System (compiler sidecar) | 1,240 | Yes |
109
109
  | `src/sourcemaps.js` | Source Map V3 Generator | 122 | Yes |
110
110
  | `src/tags.js` | HTML Tag Classification | 63 | Yes |
111
111
  | `src/parser.js` | Generated parser | 357 | No (auto-gen) |
112
- | `src/grammar/grammar.rip` | Grammar specification | 935 | Yes (carefully) |
112
+ | `src/grammar/grammar.rip` | Grammar specification | 945 | Yes (carefully) |
113
113
  | `src/grammar/solar.rip` | Parser generator | 916 | No |
114
114
 
115
115
  ## Example Flow
@@ -579,4 +579,4 @@ rip> .js # Toggle JS display
579
579
 
580
580
  ---
581
581
 
582
- *Rip 3.4 — 1,140 tests passing — Zero dependencies — Self-hosting — ~10,300 LOC*
582
+ *Rip 3.7 — 1,219 tests passing — Zero dependencies — Self-hosting — ~10,800 LOC*
package/docs/RIP-LANG.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Rip Language Reference
4
4
 
5
- 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, ~10,300 LOC.
5
+ 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, ~10,800 LOC.
6
6
 
7
7
  ---
8
8
 
@@ -121,6 +121,10 @@ else
121
121
  status = active ? "on" : "off"
122
122
  result = x > 5 ? "big" : "small"
123
123
 
124
+ # Ternary (Python-style postfix)
125
+ status = "active" if online else "offline"
126
+ label = "big" if x > 5 else "small"
127
+
124
128
  # NOTE: Subscript in ternary true-branch needs parentheses
125
129
  item = found ? (arr[0]) : default
126
130
 
@@ -227,6 +231,15 @@ until done
227
231
  loop
228
232
  data = fetch()
229
233
  break if data.complete
234
+
235
+ # Loop N times
236
+ loop 5
237
+ console.log "hi"
238
+ # Compiles to: for (let _i = 0; _i < 5; _i++) { ... }
239
+
240
+ # Loop with variable or expression
241
+ loop retries
242
+ attempt!
230
243
  ```
231
244
 
232
245
  ## Comprehensions
@@ -275,6 +288,7 @@ Multiple lines
275
288
  | `of` | `k of obj` | Object key existence |
276
289
  | `?` (postfix) | `a?` | Existence check (`a != null`) |
277
290
  | `?` (ternary) | `a ? b : c` | Ternary conditional |
291
+ | `if...else` (postfix) | `b if a else c` | Python-style ternary |
278
292
  | `?.` `?.[]` `?.()` | `a?.b` `a?.[0]` `a?.()` | Optional chaining (ES6) |
279
293
  | `?[]` `?()` | `a?[0]` `a?(x)` | Optional chaining shorthand |
280
294
  | `??` | `a ?? b` | Nullish coalescing |
@@ -293,6 +307,15 @@ Multiple lines
293
307
  | `!` | Void | `def process!` | Suppresses implicit return |
294
308
  | `!?` | Otherwise | `val !? 5` | Default if undefined or throws |
295
309
  | `=~` | Match | `str =~ /pat/` | Ruby-style regex match, captures in `_` |
310
+ | `::` | Prototype | `String::trim` | `String.prototype.trim` |
311
+ | `[-n]` | Negative index | `arr[-1]` | `arr.at(-1)` |
312
+ | `*` | String repeat | `"-" * 40` | `"-".repeat(40)` |
313
+ | `<` `<=` | Chained comparison | `1 < x < 10` | `(1 < x) && (x < 10)` |
314
+ | `\|>` | Pipe | `x \|> fn` or `x \|> fn(y)` | `fn(x)` or `fn(x, y)` |
315
+ | `.=` | Method assign | `x .= trim()` | `x = x.trim()` |
316
+ | `*` | Merge assign | `*obj = {a: 1}` | `Object.assign(obj, {a: 1})` |
317
+ | `not in` | Not in | `x not in arr` | Negated membership test |
318
+ | `not of` | Not of | `k not of obj` | Negated key existence |
296
319
 
297
320
  ## Assignment Operators
298
321
 
@@ -333,6 +356,10 @@ status = active ? 'on' : 'off'
333
356
  result = valid ? obj.field : null
334
357
  output = ready ? compute() : fallback
335
358
 
359
+ # Python-style postfix ternary
360
+ status = "active" if online else "offline"
361
+ label = "big" if x > 5 else "small"
362
+
336
363
  # Nested
337
364
  level = score > 90 ? 'A' : score > 80 ? 'B' : score > 70 ? 'C' : 'F'
338
365
 
@@ -349,6 +376,155 @@ result = riskyOperation() !? "default"
349
376
  # If riskyOperation() throws or returns null/undefined, result = "default"
350
377
  ```
351
378
 
379
+ ## Method Assignment (`.=`)
380
+
381
+ A Rip original. Compound assignment for method calls — apply a method to a
382
+ variable and assign the result back in one step:
383
+
384
+ ```coffee
385
+ # Without .= — repeat the variable name every time
386
+ items = items.filter -> it.active
387
+ items = items.map -> it.name
388
+ items = items.sort (a, b) -> a.localeCompare b
389
+ str = str.trim()
390
+ str = str.toLowerCase()
391
+
392
+ # With .= — name it once, transform in place
393
+ items .= filter -> it.active
394
+ items .= map -> it.name
395
+ items .= sort (a, b) -> a.localeCompare b
396
+ str .= trim()
397
+ str .= toLowerCase()
398
+ ```
399
+
400
+ `x .= method(args)` compiles to `x = x.method(args)`. It's the method-call
401
+ equivalent of `+=` — just as `x += 5` means `x = x + 5`, `x .= trim()`
402
+ means `x = x.trim()`.
403
+
404
+ This operator is unique to Rip. Other languages have `+=`, `-=`, `*=`, and
405
+ other arithmetic compound assignments, but none extend the concept to method
406
+ calls. Combined with implicit `it`, this enables remarkably concise data
407
+ transformation pipelines:
408
+
409
+ ```coffee
410
+ users .= filter -> it.active
411
+ users .= map -> it.name
412
+ users .= sort()
413
+ ```
414
+
415
+ Works with any method — built-in or custom, with or without arguments.
416
+
417
+ ## Merge Assignment (`*`)
418
+
419
+ A Rip original. Merge properties into an existing object without repeating
420
+ its name:
421
+
422
+ ```coffee
423
+ # Without * — repeat the object name or use verbose Object.assign
424
+ Object.assign config,
425
+ host: "localhost"
426
+ port: 3000
427
+ debug: true
428
+
429
+ # With * — clean and direct
430
+ *config =
431
+ host: "localhost"
432
+ port: 3000
433
+ debug: true
434
+
435
+ # Single line
436
+ *opts = {method: "POST", body: data}
437
+
438
+ # Dotted paths
439
+ *el.style =
440
+ color: "red"
441
+ fontSize: "14px"
442
+
443
+ # Merge user overrides into defaults
444
+ *defaults = userConfig
445
+ ```
446
+
447
+ `*target = value` compiles to `Object.assign(target, value)`. The `*` reads
448
+ as "spread these into" — the same concept as `...` spread but as an
449
+ assignment. This is unique to Rip.
450
+
451
+ Common use cases: config objects, options bags, state initialization, DOM
452
+ styling, merging defaults with overrides — anywhere you're setting multiple
453
+ properties on an existing object.
454
+
455
+ ## Prototype Operator (`::`)
456
+
457
+ Access `.prototype` with `::` (CoffeeScript-style). Disambiguated from type annotations by spacing:
458
+
459
+ ```coffee
460
+ # Prototype access (no space after ::)
461
+ String::starts = String::startsWith
462
+ String::ends = String::endsWith
463
+ String::has = String::includes
464
+
465
+ # Now you can use them
466
+ "hello".starts "he" # true
467
+ "hello.rip".ends ".rip" # true
468
+ "error: bad".has "error" # true
469
+
470
+ # Define new prototype methods
471
+ String::shout = -> @toUpperCase() + "!"
472
+ "hello".shout() # "HELLO!"
473
+
474
+ # Type annotations (space after ::) — unaffected
475
+ name:: string = "Alice"
476
+ def greet(name:: string):: string
477
+ "Hello, #{name}!"
478
+ ```
479
+
480
+ The rule is simple: `::` with **no space** before an identifier is prototype access. `::` with a **space** is a type annotation.
481
+
482
+ ## Negative Indexing
483
+
484
+ Literal negative indexes compile to `.at()` for Python-style access from the end:
485
+
486
+ ```coffee
487
+ arr = [10, 20, 30, 40, 50]
488
+
489
+ arr[-1] # → arr.at(-1) — 50 (last)
490
+ arr[-2] # → arr.at(-2) — 40 (second to last)
491
+ str[-1] # works on strings too
492
+
493
+ arr?[-1] # → arr?.at(-1) — optional variant
494
+
495
+ # Positive and variable indexes are unchanged
496
+ arr[0] # → arr[0] — normal access
497
+ arr[i] # → arr[i] — variable index
498
+ ```
499
+
500
+ Only literal negative numbers trigger the `.at()` transform. Variable indexes pass through as-is.
501
+
502
+ ## Pipe Operator (`|>`)
503
+
504
+ Pipes a value into a function as its first argument. Chains left-to-right:
505
+
506
+ ```coffee
507
+ # Simple reference — value becomes the sole argument
508
+ 5 |> double # → double(5)
509
+ 10 |> Math.sqrt # → Math.sqrt(10)
510
+ "hello" |> console.log # → console.log("hello")
511
+
512
+ # Multi-arg — value is inserted as the FIRST argument
513
+ 5 |> add(3) # → add(5, 3)
514
+ data |> filter(isActive) # → filter(data, isActive)
515
+ "World" |> greet("!") # → greet("World", "!")
516
+
517
+ # Chaining — reads left-to-right like a pipeline
518
+ 5 |> double |> add(1) |> console.log
519
+ # → console.log(add(double(5), 1))
520
+
521
+ # Works with dotted methods
522
+ users |> Array.from # → Array.from(users)
523
+ data |> JSON.stringify(null, 2) # → JSON.stringify(data, null, 2)
524
+ ```
525
+
526
+ This is the **Elixir-style** pipe — strictly better than F#'s (which only supports bare function references) and cleaner than Hack's (which requires a `%` placeholder). No special syntax to learn; if the right side is a call, the left value goes first.
527
+
352
528
  ---
353
529
 
354
530
  # 4. Functions
@@ -420,6 +596,41 @@ def findUser(id)
420
596
  null
421
597
  ```
422
598
 
599
+ ## Implicit `it` Parameter
600
+
601
+ Arrow functions with no explicit parameters that reference `it` in the body automatically inject `it` as the parameter:
602
+
603
+ ```coffee
604
+ # Without `it` — must name a throwaway variable
605
+ users.filter (u) -> u.active
606
+ names = users.map (u) -> u.name
607
+
608
+ # With `it` — cleaner
609
+ users.filter -> it.active
610
+ names = users.map -> it.name
611
+ orders.filter -> it.total > 100
612
+
613
+ # Works with fat arrows too
614
+ items.map => it.toUpperCase()
615
+
616
+ # Nested arrows — each level gets its own `it`
617
+ # Only the innermost param-less arrow with `it` is affected
618
+ groups.map -> it.items.filter -> it.active
619
+
620
+ # Explicit params still work normally
621
+ items.sort (a, b) -> a - b
622
+ ```
623
+
624
+ Compiles to standard JavaScript — `it` becomes a regular function parameter:
625
+
626
+ ```coffee
627
+ arr.filter -> it > 5
628
+ # → arr.filter(function(it) { return (it > 5); })
629
+
630
+ arr.map => it.name
631
+ # → arr.map(it => it.name)
632
+ ```
633
+
423
634
  ## Calling Functions
424
635
 
425
636
  ```coffee
@@ -1165,4 +1376,4 @@ count = 10 # Logs: "Count: 10, Doubled: 20"
1165
1376
 
1166
1377
  ---
1167
1378
 
1168
- *Rip 3.4 — 1,140 tests passing — Zero dependencies — Self-hosting — ~10,300 LOC*
1379
+ *Rip 3.7 — 1,219 tests passing — Zero dependencies — Self-hosting — ~10,800 LOC*