Skip to content
HN On Hacker News ↗

GitHub - shreeve/rip-lang: A modern language that compiles to JavaScript

▲ 28 points 16 comments by perfunctory 5w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is a mix of AI-generated, and human-written content

50 %

AI likelihood · overall

Mixed
49% human-written 51% AI-generated
SEGMENTS · HUMAN 1 of 6
SEGMENTS · AI 2 of 6
WORD COUNT 1,459
PEAK AI % 100% · §3
Analyzed
Apr 23
backend: pangram/v3.3
Segments scanned
6 windows
avg 243 words each
Distribution
49 / 51%
human / AI fraction
Verdict
Mixed
Pangram v3.3

Article text · 1,459 words · 6 segments analyzed

Human AI-generated
§1 Human · 27%

A modern language that compiles to JavaScript

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 11,000 lines of code.

No imports. No hooks. No dependency arrays. Just write code.

data = fetchUsers! # Dammit operator (call + await) user = User.new name: "Alice" # Ruby-style constructor squares = (x * x for x in [1..10]) # List comprehension

str =~ /Hello, (\w+)/ # Regex match log "Found: #{_[1]}" # Captures in _[1], _[2], etc.

get '/users/:id' -> # RESTful API endpoint, comma-less name = read 'name', 'string!' # Required string age = read 'age' , [0, 105] # Simple numeric validation

What makes Rip different:

Modern output — ES2022 with native classes, ?., ??, modules New operators — !, //, %%, =~, |>, .new(), and more Reactive operators — :=, ~=, ~> as language syntax Optional types — :: annotations, type aliases, .d.ts emission Zero dependencies — everything included, even the parser generator Self-hosting — bun run parser rebuilds the compiler from source

Installation bun add -g rip-lang # Install globally rip # Interactive REPL rip file.rip # Run a file rip -c file.rip # Compile to JavaScript

Extensions Pre-built binaries and VS Code / Cursor extensions are published via GitHub Pages — see the extensions hub for details.

§2 Mixed · 46%

ripdb — DuckDB extension that exposes a rip-db server as a first-class attached database: SET allow_unsigned_extensions = true; INSTALL ripdb FROM 'https://shreeve.github.io/rip-lang/extensions/duckdb'; LOAD ripdb; rip-lang.print — syntax-highlighted source printer for VS Code / Cursor: curl -LO https://shreeve.github.io/rip-lang/extensions/vscode/print/print-latest.vsix cursor --install-extension ./print-latest.vsix rip-lang.rip — Rip language support for VS Code / Cursor: curl -LO https://shreeve.github.io/rip-lang/extensions/vscode/rip/rip-latest.vsix cursor --install-extension ./rip-latest.vsix

Language Functions & Classes def greet(name) # Named function "Hello, #{name}!"

add = (a, b) -> a + b # Arrow function handler = (e) => @process e # Fat arrow (preserves this)

class Dog extends Animal speak: -> log "#{@name} barks"

dog = Dog.new("Buddy") # Ruby-style constructor String Interpolation "Hello, #{name}!" # CoffeeScript-style "Hello, ${name}!" # JavaScript-style "#{a} + #{b} = #{a + b}" # Expressions work in both Both #{} and ${} compile to JavaScript template literals. Use whichever you prefer. Objects user = {name: "Alice", age: 30} config = api.endpoint: "https://example.com" # Dotted keys become flat string keys api.timeout: 5000 # {'api.endpoint': "...", 'api.timeout': 5000} Destructuring & Comprehensions {name, age} = person [first, ...rest] = items

squares =

§3 AI · 100%

(x * x for x in [1..10]) # Array comprehension console.log x for x in items # Loop (no array) Async & Chaining def loadUser(id) response = await fetch "/api/#{id}" await response.json()

user?.profile?.name # Optional chaining el?.scrollTop = 0 # Optional chain assignment data = fetchData! # Await shorthand Iteration for item in [1, 2, 3] # Array iteration (for-in) console.log item

for key, value of object # Object iteration (for-of) console.log "#{key}: #{value}"

for x as iterable # ES6 for-of on any iterable console.log x

for x as! asyncIterable # Async iteration shorthand console.log x # Equivalent to: for await x as asyncIterable

loop # Infinite loop (while true) process! loop 5 # Repeat N times console.log "hi" Implicit it Arrow functions with no params that reference it auto-inject it as the parameter: users.filter -> it.active # → users.filter(function(it) { ... }) names = users.map -> it.name # no need to name a throwaway variable orders.filter -> it.total > 100 # works with any expression Reactivity State, computed values, and effects as language operators:

Operator Mnemonic Example What it does

= "gets value" x = 5 Regular assignment

:= "gets state" count := 0 Reactive state container

~= "always equals" twice ~= count * 2 Auto-updates on changes

~> "always calls" ~> log count Runs on dependency changes

=! "equals, dammit!" MAX =! 100 Readonly constant

Types (Optional) Type annotations are erased at compile time — zero runtime cost: def greet(name:: string):: string # Typed function "Hello, #{name}!"

§4 Mixed · 46%

type User = # Structural type id: number name: string

enum HttpCode # Runtime enum ok = 200 notFound = 404 Compiles to .js (types erased) + .d.ts (types preserved) — full IDE support via TypeScript Language Server. See docs/RIP-TYPES.md. Standard Library 13 global helpers available in every Rip program — no imports needed: p "hello" # console.log shorthand pp {name: "Alice", age: 30} # pretty-print JSON (also returns value) warn "deprecated" # console.warn assert x > 0, "must be positive" raise TypeError, "expected string" todo "finish this later" kind [1, 2, 3] # "array" (fixes typeof) rand 10 # 0-9 rand 5, 10 # 5-10 inclusive sleep! 1000 # await sleep(1000) exit 1 # process.exit(1) abort "fatal" # log to stderr + exit(1) zip names, ages # [[n1,a1], [n2,a2], ...] noop # () => {} All use globalThis with ??= — override any by redeclaring locally.

Operators

Operator Example What it does

! (dammit) fetchData! Calls AND awaits

! (void) def process! Suppresses implicit return

?! (presence) @checked?! True if truthy, else undefined (Houdini operator)

? (existence) x? True if x != null

?: (ternary) x > 0 ? 'yes' : 'no' JS-style ternary expression

if...else (postfix) "yes" if cond else "no" Python-style ternary expression

?. ?.[] ?.() a?.b a?.[0] a?.() Optional chaining (ES6)

?[] ?() a?[0] a?(x) Optional chaining shorthand

?. = el?.scrollTop = 0 Optional chain assignment — guarded write

= (render) = item.textContent Expression output as text node in render blocks

??

§5 Mixed · 34%

a ?? b Nullish coalescing

... (spread) [...items, last] Prefix spread (ES6)

// 7 // 2 Floor division

%% -1 %% 3 True modulo

=~ str =~ /Hello, (\w+)/ Match (captures in _)

[//, n] str[/Hello, (\w+)/, 1] Extract capture n

.new() Dog.new() Ruby-style constructor

:: (prototype) String::trim String.prototype.trim

[-n] (negative index) arr[-1] Last element via .at()

* (string repeat) "-" * 40 String repeat via .repeat()

< <= (chained) 1 < x < 10 Chained comparisons

|> (pipe) x |> fn or x |> fn(y) Pipe operator (first-arg insertion)

not in x not in arr Negated membership test

not of k not of obj Negated key existence

.= (method assign) x .= trim() x = x.trim() — compound method assignment

*> (merge assign) *>obj = {a: 1} Object.assign(obj, {a: 1})

or return x = get() or return err Guard clause (Ruby-style)

?? throw x = get() ?? throw err Nullish guard

:name (symbol) :redo, :active Ruby-style interned symbol (Symbol.for)

Heredoc & Heregex Heredoc — The closing ''' or """ position defines the left margin. All content is dedented relative to the column where the closing delimiter sits: html = ''' <div> <p>Hello</p> </div> ''' # Closing ''' at column 4 (same as content) — no leading whitespace # Result: "<div>\n <p>Hello</p>\n</div>"

html = ''' <div> <p>Hello</p> </div> ''' # Closing ''' at column 2 — 2 spaces of leading whitespace preserved # Result: " <div>\n <p>Hello</p>\n </div>" Raw heredoc — Append \ to the opening delimiter ('''\ or """\) to prevent escape processing.

§6 AI · 100%

Backslash sequences like \n, \t, \u stay literal: script = '''\ echo "hello\nworld" sed 's/\t/ /g' file.txt \''' # \n and \t stay as literal characters, not newline/tab Heregex — Extended regex with comments and whitespace: pattern = /// ^(\d{3}) # area code -(\d{4}) # number ///

Schema Rip Schema is a first-class language construct for declaring data inline. One keyword — schema — covers what would otherwise take three libraries: a validator (Zod-style), an ORM (Prisma/ActiveRecord-style), and a migration tool. Schemas live in .rip source, compile alongside the rest of your code, and are real runtime values you can export, pass around, and derive from. Unlike Rip's compile-time type / interface system (which is erased from JS output), schemas exist at runtime because they validate, construct class instances, run ORM queries, and emit SQL — all from a single declaration that your editor also type-checks via automatic shadow TypeScript. A schema has one of five kinds, selected by a :symbol after the keyword. :input (the default) is a field validator. :shape adds methods and computed getters — validators with behavior, like a Money or Address value. :enum declares a closed set of members using :symbol literals (:draft, :active 1) and exposes .parse() that accepts either the member name or its value. :mixin declares a reusable field group — non-instantiable, consumed by other schemas via @mixin Name with diamond-dedup and cycle detection. :model is the big one: DB-backed, with a full async ORM (find, where, create, save, destroy), migration-grade DDL emission (toSQL), Rails-ordered lifecycle hooks (ten recognized names from beforeValidation through afterDestroy), and @belongs_to / @has_many / @has_one relations that resolve lazily through a process-global registry. # Validator SignupInput = schema email! email password! string, 8..100

# Shape with behavior Address = schema :shape street!