rip-lang 3.13.27 → 3.13.28

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/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-3.13.27-blue.svg" alt="Version"></a>
12
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-3.13.28-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
14
  <a href="#"><img src="https://img.shields.io/badge/tests-1%2C300%2F1%2C300-brightgreen.svg" alt="Tests"></a>
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
@@ -205,6 +205,7 @@ All use `globalThis` with `??=` — override any by redeclaring locally.
205
205
  | `!` (void) | `def process!` | Suppresses implicit return |
206
206
  | `!?` (otherwise) | `val !? 5` | Default only if `undefined` (infix) |
207
207
  | `!?` (defined) | `val!?` | True if not `undefined` (postfix) |
208
+ | `?!` (presence) | `@checked?!` | True if truthy, else `undefined` (Houdini operator) |
208
209
  | `?` (existence) | `x?` | True if `x != null` |
209
210
  | `?:` (ternary) | `x > 0 ? 'yes' : 'no'` | JS-style ternary expression |
210
211
  | `if...else` (postfix) | `"yes" if cond else "no"` | Python-style ternary expression |
@@ -417,14 +418,14 @@ Rip includes optional packages for full-stack development:
417
418
 
418
419
  | Package | Version | Purpose |
419
420
  |---------|---------|---------|
420
- | [rip-lang](https://www.npmjs.com/package/rip-lang) | 3.13.26 | Core language compiler |
421
- | [@rip-lang/server](packages/server/) | 1.2.11 | Multi-worker app server (web framework, hot reload, HTTPS, mDNS) |
422
- | [@rip-lang/db](packages/db/) | 1.3.13 | DuckDB server with official UI + ActiveRecord-style client |
423
- | [@rip-lang/grid](packages/grid/) | 0.2.8 | Reactive data grid |
424
- | [@rip-lang/swarm](packages/swarm/) | 1.2.16 | Parallel job runner with worker pool |
425
- | [@rip-lang/csv](packages/csv/) | 1.3.4 | CSV parser + writer |
426
- | [@rip-lang/schema](packages/schema/) | 0.3.6 | Unified schema → TypeScript types, SQL DDL, validation, ORM |
427
- | [VS Code Extension](packages/vscode/) | 0.5.0 | Syntax highlighting, type intelligence, source maps |
421
+ | [rip-lang](https://www.npmjs.com/package/rip-lang) | 3.13.27 | Core language compiler |
422
+ | [@rip-lang/server](packages/server/) | 1.3.12 | Multi-worker app server (web framework, hot reload, HTTPS, mDNS) |
423
+ | [@rip-lang/db](packages/db/) | 1.3.15 | DuckDB server with official UI + ActiveRecord-style client |
424
+ | [@rip-lang/grid](packages/grid/) | 0.2.10 | Reactive data grid |
425
+ | [@rip-lang/swarm](packages/swarm/) | 1.2.18 | Parallel job runner with worker pool |
426
+ | [@rip-lang/csv](packages/csv/) | 1.3.6 | CSV parser + writer |
427
+ | [@rip-lang/schema](packages/schema/) | 0.3.8 | Unified schema → TypeScript types, SQL DDL, validation, ORM |
428
+ | [VS Code Extension](packages/vscode/) | 0.5.7 | Syntax highlighting, type intelligence, source maps |
428
429
 
429
430
  ```bash
430
431
  bun add -g @rip-lang/db # Installs everything (rip-lang + server + db)
@@ -473,9 +474,9 @@ bun run build # Build browser bundle
473
474
 
474
475
  | Guide | Description |
475
476
  |-------|-------------|
476
- | [docs/RIP-LANG.md](docs/RIP-LANG.md) | Full language reference (syntax, operators, reactivity, types, future ideas) |
477
- | [docs/RIP-INTERNALS.md](docs/RIP-INTERNALS.md) | Compiler architecture (lexer, parser, codegen, S-expressions) |
477
+ | [docs/RIP-LANG.md](docs/RIP-LANG.md) | Full language reference (syntax, operators, reactivity, types, components) |
478
478
  | [docs/RIP-TYPES.md](docs/RIP-TYPES.md) | Type system specification |
479
+ | [AGENTS.md](AGENTS.md) | Compiler architecture, S-expressions, component system internals |
479
480
  | [AGENTS.md](AGENTS.md) | AI agents — get up to speed for working on the compiler |
480
481
 
481
482
  ---
package/bin/rip CHANGED
@@ -69,6 +69,7 @@ Examples:
69
69
  rip -m example.rip # Compile with inline source map
70
70
  rip -cd example.rip # Show compiled JS and type declarations
71
71
  rip -q -c example.rip # Just the JS, no headers (for piping)
72
+ echo 'p 1 + 2' | rip # Execute from stdin
72
73
  echo 'x = 1 + 2' | rip -c # Compile from stdin
73
74
 
74
75
  Shebang support:
@@ -172,14 +173,29 @@ async function main() {
172
173
  }
173
174
  }
174
175
 
175
- // Execute script directly (no compile flags)
176
+ // Stdin: write to temp .rip file so it flows through the normal execution/compile paths
176
177
  const hasCompileFlag = showCompiled || showTokens || showSExpr || generateDts || generateMap || outputFile;
177
- if (inputFile && !hasCompileFlag && isFile(inputFile)) {
178
+ let isStdin = false;
179
+ if (!inputFile) {
180
+ const tmp = join(process.cwd(), '.__rip_stdin__.rip');
181
+ try {
182
+ writeFileSync(tmp, readFileSync(0, 'utf-8'));
183
+ inputFile = tmp;
184
+ isStdin = true;
185
+ } catch (error) {
186
+ console.error('Error reading stdin:', error.message);
187
+ process.exit(1);
188
+ }
189
+ }
190
+
191
+ // Execute script directly (no compile flags)
192
+ if (!hasCompileFlag && isFile(inputFile)) {
178
193
  if (inputFile.endsWith('.rip')) {
179
194
  const result = spawnSync('bun', ['--preload', loaderPath, inputFile, ...scriptArgs], {
180
195
  stdio: 'inherit',
181
196
  env: process.env
182
197
  });
198
+ if (isStdin) try { unlinkSync(inputFile); } catch {}
183
199
  process.exit(result.status ?? 1);
184
200
  }
185
201
 
@@ -201,7 +217,7 @@ async function main() {
201
217
 
202
218
  // Fallback: look for bin/{command} in git repo root
203
219
  // Allows `rip migrate --status` to find and run {repo}/bin/migrate
204
- if (inputFile && !inputFile.startsWith('-') && !isFile(inputFile)) {
220
+ if (!isStdin && inputFile && !inputFile.startsWith('-') && !isFile(inputFile)) {
205
221
  try {
206
222
  const repoRoot = execSync('git rev-parse --show-toplevel', {
207
223
  encoding: 'utf-8',
@@ -215,33 +231,26 @@ async function main() {
215
231
  } catch {}
216
232
  }
217
233
 
218
- // --- Compile ---
234
+ // --- Compile (with flags) ---
219
235
 
220
- let source;
236
+ if (!isFile(inputFile)) {
237
+ console.error(`Error: File not found: ${inputFile}`);
238
+ process.exit(1);
239
+ }
221
240
 
222
- try {
223
- if (inputFile) {
224
- if (!isFile(inputFile)) {
225
- console.error(`Error: File not found: ${inputFile}`);
226
- process.exit(1);
227
- }
228
- source = readFileSync(inputFile, 'utf-8');
229
- } else {
230
- source = readFileSync(0, 'utf-8');
231
- }
241
+ let source = readFileSync(inputFile, 'utf-8');
232
242
 
243
+ try {
233
244
  const compiler = new Compiler({ showTokens, showSExpr, quiet,
234
245
  types: generateDts ? 'emit' : undefined,
235
246
  sourceMap: generateMap ? 'inline' : undefined,
236
247
  });
237
248
  const result = compiler.compile(source);
238
249
 
239
- const shouldShowCompiled = showCompiled || (!showTokens && !showSExpr && !generateDts) || outputFile;
240
-
241
250
  if (outputFile) {
242
251
  writeFileSync(outputFile, result.code, 'utf-8');
243
252
  if (!quiet) console.log(`Compiled to ${outputFile}`);
244
- } else if (shouldShowCompiled) {
253
+ } else if (showCompiled) {
245
254
  if (!quiet) console.log(`// == JavaScript output by Rip ${VERSION} == //\n`);
246
255
  console.log(result.code);
247
256
  }
@@ -254,6 +263,8 @@ async function main() {
254
263
  console.error('Compilation Error:', error.message);
255
264
  if (error.stack) console.error(error.stack);
256
265
  process.exit(1);
266
+ } finally {
267
+ if (isStdin) try { unlinkSync(inputFile); } catch {}
257
268
  }
258
269
  }
259
270
 
package/docs/RIP-LANG.md CHANGED
@@ -295,6 +295,7 @@ Multiple lines
295
295
  | `of` | `k of obj` | Object key existence |
296
296
  | `?` (postfix) | `a?` | Existence check (`a != null`) |
297
297
  | `!?` (postfix) | `a!?` | Defined check (`a !== undefined`) |
298
+ | `?!` (postfix) | `a?!` | Presence check — true if truthy, else undefined |
298
299
  | `?` (ternary) | `a ? b : c` | Ternary conditional |
299
300
  | `if...else` (postfix) | `b if a else c` | Python-style ternary |
300
301
  | `?.` `?.[]` `?.()` | `a?.b` `a?.[0]` `a?.()` | Optional chaining (ES6) |
@@ -315,6 +316,7 @@ Multiple lines
315
316
  | `!` | Void | `def process!` | Suppresses implicit return |
316
317
  | `!?` | Otherwise | `val !? 5` | Default if undefined (infix) |
317
318
  | `!?` | Defined | `val!?` | True if not undefined (postfix) |
319
+ | `?!` | Presence | `@checked?!` | `(this.checked ? true : undefined)` — Houdini operator |
318
320
  | `=~` | Match | `str =~ /pat/` | Ruby-style regex match, captures in `_` |
319
321
  | `::` | Prototype | `String::trim` | `String.prototype.trim` |
320
322
  | `[-n]` | Negative index | `arr[-1]` | `arr.at(-1)` |
@@ -404,6 +406,40 @@ The postfix form mirrors `?` (existence check) but with tighter semantics:
404
406
  |----------|--------|--------|-------------|-----|---------|------|
405
407
  | `v?` | not nullish | false | false | true | true | true |
406
408
  | `v!?` | not undefined | true | false | true | true | true |
409
+ | `v?!` | truthy presence | `undefined` | `undefined` | `undefined` | `undefined` | `undefined` |
410
+
411
+ Note: `v?!` returns `true` for truthy values, `undefined` for falsy values.
412
+
413
+ ## Presence Operator (`?!`) — The Houdini
414
+
415
+ The `?!` operator (postfix, unspaced) returns `true` if the value is truthy,
416
+ or `undefined` if it's falsy. Now you see it… now you don't.
417
+
418
+ ```coffee
419
+ @checked?! # (this.checked ? true : undefined)
420
+ (idx is active)?! # ((idx === active) ? true : undefined)
421
+ ```
422
+
423
+ Designed for `data-*` attributes in headless UI components, where falsy values
424
+ need to *remove* the attribute rather than set it to `"false"`:
425
+
426
+ ```coffee
427
+ # Before — verbose and repetitive
428
+ data-checked: (@checked or undefined),
429
+ data-disabled: (@disabled or undefined),
430
+
431
+ # After — clean and expressive
432
+ data-checked: @checked?!,
433
+ data-disabled: @disabled?!,
434
+ ```
435
+
436
+ Works with any expression, not just identifiers:
437
+
438
+ ```coffee
439
+ data-highlighted: (idx is highlightedIndex)?!,
440
+ data-selected: (opt.value is String(@value))?!,
441
+ data-active: (tab is @active)?!,
442
+ ```
407
443
 
408
444
  ## Method Assignment (`.=`)
409
445
 
@@ -1337,6 +1373,225 @@ Counter = component
1337
1373
  button @click: @increment, "+"
1338
1374
  ```
1339
1375
 
1376
+ ### Component Features
1377
+
1378
+ **State and Computed:**
1379
+
1380
+ ```coffee
1381
+ App = component
1382
+ count := 0 # reactive state
1383
+ doubled ~= count * 2 # computed (auto-updates)
1384
+ label =! "Counter" # readonly (const)
1385
+ ```
1386
+
1387
+ **Public Props (passed from parent):**
1388
+
1389
+ ```coffee
1390
+ Card = component
1391
+ @title =! "Untitled" # readonly prop with default
1392
+ @count := 0 # reactive prop (two-way capable)
1393
+ ```
1394
+
1395
+ ```coffee
1396
+ # Parent passes props
1397
+ Card title: "Hello", count: 42
1398
+ ```
1399
+
1400
+ **Methods:**
1401
+
1402
+ ```coffee
1403
+ App = component
1404
+ count := 0
1405
+ inc = -> @count += 1
1406
+ add = (n) -> @count += n
1407
+ ```
1408
+
1409
+ **Lifecycle Hooks:**
1410
+
1411
+ ```coffee
1412
+ App = component
1413
+ beforeMount = -> p "about to mount"
1414
+ mounted = -> p "mounted"
1415
+ updated = -> p "updated"
1416
+ beforeUnmount = -> p "about to unmount"
1417
+ unmounted = -> p "unmounted"
1418
+ onError = (err, comp) -> p "caught: #{err.message}"
1419
+ ```
1420
+
1421
+ **Effects:**
1422
+
1423
+ ```coffee
1424
+ App = component
1425
+ count := 0
1426
+ ~> p "count is now #{count}" # re-runs when count changes
1427
+ ```
1428
+
1429
+ **Render Blocks — Template Syntax:**
1430
+
1431
+ ```coffee
1432
+ App = component
1433
+ name := "world"
1434
+ render
1435
+ div.card # element with class
1436
+ h1#title "Hello" # element with id
1437
+ span name # reactive text
1438
+ input value <=> name # two-way binding
1439
+ button @click: -> @name = "Rip"
1440
+ "Click me"
1441
+ ```
1442
+
1443
+ **Conditional Rendering:**
1444
+
1445
+ ```coffee
1446
+ App = component
1447
+ show := true
1448
+ render
1449
+ if show
1450
+ div "Visible!"
1451
+ else
1452
+ div "Hidden"
1453
+ ```
1454
+
1455
+ **List Rendering:**
1456
+
1457
+ ```coffee
1458
+ App = component
1459
+ items := ["Apple", "Banana", "Cherry"]
1460
+ render
1461
+ ul
1462
+ for item, i in items
1463
+ li item
1464
+ ```
1465
+
1466
+ Lists use keyed reconciliation with LIS (Longest Increasing Subsequence) diffing — only items that actually move get repositioned. Appending to a list is nearly zero-cost.
1467
+
1468
+ **Child Components and Slots:**
1469
+
1470
+ ```coffee
1471
+ Card = component
1472
+ @title =! "Card"
1473
+ @children =! null
1474
+ render
1475
+ div.card
1476
+ h2 @title
1477
+ @children
1478
+
1479
+ App = component
1480
+ render
1481
+ Card title: "Welcome"
1482
+ p "This is the card body"
1483
+ ```
1484
+
1485
+ **CSS Transitions:**
1486
+
1487
+ Add `~name` to any element inside a conditional block for enter/leave animations:
1488
+
1489
+ ```coffee
1490
+ App = component
1491
+ show := true
1492
+ render
1493
+ button @click: -> @show = !show
1494
+ "Toggle"
1495
+ if show
1496
+ div ~fade
1497
+ p "I fade in and out"
1498
+ ```
1499
+
1500
+ Built-in presets:
1501
+
1502
+ | Preset | Effect |
1503
+ |--------|--------|
1504
+ | `~fade` | Opacity fade |
1505
+ | `~slide` | Slide up/down with opacity |
1506
+ | `~scale` | Scale from 95% with opacity |
1507
+ | `~blur` | Blur with opacity |
1508
+ | `~fly` | Fly in/out from distance with opacity |
1509
+
1510
+ Custom transitions work with any name — just provide your own CSS:
1511
+
1512
+ ```css
1513
+ .wobble-enter-active, .wobble-leave-active { transition: transform 0.3s ease; }
1514
+ .wobble-enter-from { transform: rotate(-5deg); }
1515
+ .wobble-leave-to { transform: rotate(5deg); }
1516
+ ```
1517
+
1518
+ ```coffee
1519
+ div ~wobble
1520
+ span "Custom animation"
1521
+ ```
1522
+
1523
+ **Error Boundaries:**
1524
+
1525
+ The `onError` lifecycle hook catches errors from child components:
1526
+
1527
+ ```coffee
1528
+ App = component
1529
+ onError = (err, source) ->
1530
+ p "Error in #{source}: #{err.message}"
1531
+ render
1532
+ ChildThatMightFail
1533
+ ```
1534
+
1535
+ Errors walk up the component tree (`_parent` chain) to the nearest `onError` handler. Without a boundary, errors throw normally.
1536
+
1537
+ **Context API:**
1538
+
1539
+ Share data across the component tree without prop drilling:
1540
+
1541
+ ```coffee
1542
+ ThemeProvider = component
1543
+ ~> setContext "theme", "dark"
1544
+
1545
+ ThemedButton = component
1546
+ theme =! getContext "theme"
1547
+ render
1548
+ button class: theme
1549
+ ```
1550
+
1551
+ **SVG Rendering:**
1552
+
1553
+ SVG elements use `createElementNS` automatically:
1554
+
1555
+ ```coffee
1556
+ Icon = component
1557
+ render
1558
+ svg.icon
1559
+ path d: "M10 10 L20 20"
1560
+ circle cx: "50", cy: "50", r: "40"
1561
+ ```
1562
+
1563
+ **Dynamic Classes:**
1564
+
1565
+ ```coffee
1566
+ App = component
1567
+ isActive := true
1568
+ render
1569
+ div.card class: (isActive && "active")
1570
+ div.("card", isActive && "active") # .() syntax
1571
+ ```
1572
+
1573
+ **Hyphenated Attributes:**
1574
+
1575
+ ```coffee
1576
+ div data-testid: "main", aria-label: "content"
1577
+ ```
1578
+
1579
+ **DOM Properties:**
1580
+
1581
+ ```coffee
1582
+ div innerHTML: content # reactive innerHTML
1583
+ div textContent: text # reactive textContent
1584
+ ```
1585
+
1586
+ **Element Refs:**
1587
+
1588
+ ```coffee
1589
+ App = component
1590
+ render
1591
+ canvas ref: "canvas"
1592
+ mounted = -> p @canvas # access the DOM element
1593
+ ```
1594
+
1340
1595
  ## @rip-lang/db — DuckDB Server + Client
1341
1596
 
1342
1597
  HTTP server for DuckDB with the official DuckDB UI built in, plus an ActiveRecord-style client library.
@@ -1733,6 +1988,7 @@ X.new(a: 1)
1733
1988
  a! # await a()
1734
1989
  a !? b # a if defined, else b (infix otherwise)
1735
1990
  a!? # true if a is defined (postfix defined check)
1991
+ a?! # true if truthy, else undefined (Houdini)
1736
1992
  a // b # floor divide
1737
1993
  a %% b # true modulo
1738
1994
  a =~ /pat/ # regex match, captures in _
@@ -1824,8 +2080,7 @@ Each would need design discussion before building.
1824
2080
  |----------|---------|
1825
2081
  | **[RIP-LANG.md](RIP-LANG.md)** | Full language reference (this file) |
1826
2082
  | **[RIP-TYPES.md](RIP-TYPES.md)** | Type system specification |
1827
- | **[RIP-INTERNALS.md](RIP-INTERNALS.md)** | Compiler architecture & design decisions |
1828
- | **[AGENTS.md](../AGENTS.md)** | AI agent guide for working on the compiler |
2083
+ | **[AGENTS.md](../AGENTS.md)** | Compiler architecture, S-expressions, AI agent guide |
1829
2084
 
1830
2085
  ---
1831
2086
 
package/docs/RIP-TYPES.md CHANGED
@@ -1978,4 +1978,4 @@ The type system uses a two-phase approach:
1978
1978
 
1979
1979
  **See Also:**
1980
1980
  - [RIP-LANG.md](RIP-LANG.md) — Language reference (includes reactivity and types)
1981
- - [RIP-INTERNALS.md](RIP-INTERNALS.md) — Compiler internals
1981
+ - [AGENTS.md](../AGENTS.md) — Compiler architecture, S-expressions, design decisions