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,typealiases,.d.tsemission - Zero dependencies — everything included, even the parser generator
- Self-hosting —
bun run parserrebuilds the compiler from source
Installation
bun add -g rip-lang # Install globallyrip # 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.
ripdb — DuckDB extension that exposes a rip-db server as a first-class attached database:
duckdb -unsigned # required for custom-repo extensionsINSTALL 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 = (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}!" 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 |
?? |
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. 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! string city! string full: ~> "#{@street}, #{@city}" # Enumeration Status = schema :pending 0 :active 1 :done 2 # DB-backed model User = schema :model name! string email!# email @timestamps @has_many Order beforeValidation: -> @email = @email.toLowerCase()
The body syntax is declarative, not general Rip code. Five line forms are legal: fields (name! type, min..max, with inline transforms via name! type, -> fn(it) where it is the whole raw input), directives (@timestamps, @mixin Name, @belongs_to User?), methods (name: -> body), computed getters (name: ~> body), and eager-derived fields (name: !> body — computed once at parse/hydrate, stored as an own property, distinct from ~> which re-evaluates on every access). Modifiers !, #, ? mark required, unique, and optional; the type slot is optional and defaults to string. Constraints are self-identifying by shape: min..max for ranges, [value] for defaults, /regex/ for patterns, {key: val} for attrs, and the terminal -> body for transforms. Literal-union types ("M" | "F" | "U") in the type slot cover enum-style value sets. Cross-field invariants — "passwords must match", "end after start", "id OR full-object" — attach as @ensure "message", (u) -> predicate (or an array of such pairs), run after field validation, and collect all failures in declaration order. Every instantiable schema exposes the same three-method runtime API: .parse(data) returns a cleaned value or throws SchemaError with structured .issues; .safe(data) returns {ok, value, errors} without throwing; .ok(data) is a boolean fast path that allocates no error arrays. All three have async dammit variants — User.find! 1, user.save! — that are the idiomatic form in Rip source.
:model is where the pieces converge. One declaration gives you a validator, a class with fields as enumerable own properties and methods/getters on the prototype, a chainable async query builder (User.where(active: true).order("last_name").all!), migration DDL that works standalone (User.toSQL() never touches the database), belongs-to/has-many accessors that resolve cross-module through the registry, and full shadow TypeScript with ModelSchema<Instance, Data> typing that propagates through schema algebra. Hydrated instances carry both snake_case and camelCase aliases on DB-derived columns (order.user_id and order.userId read the same slot), so raw SQL helpers and ORM access coexist cleanly. A single-function adapter interface (adapter.query(sql, params)) routes all database I/O, so tests use in-memory mocks and production uses rip-db without the ORM caring.
Schema algebra — .pick, .omit, .partial, .required, .extend — always returns a new :shape. Field semantics (type, literal unions, constraints, inline transforms) carry through to the derived shape; instance behavior (methods, computed ~>, eager-derived !>, hooks, and @ensure refinements) does not. User.omit "password" produces a validator for User minus the password field; it won't have .find() or the beforeSave hook, but field-level transforms (email, -> it.email.toLowerCase()) continue to fire on the derived shape exactly as they did on the original. This invariant is enforced both at runtime (ORM methods throw on derived shapes with a targeted diagnostic pointing at query projection) and at the TypeScript level (algebra generics are parameterized over Data, not Instance, so the derived types correctly omit methods and ORM surface). Internally, the whole feature is a compiler sidecar — 54% of the implementation lives in src/schema/schema.js and touches the core compiler in under 100 lines of wiring. A four-layer lazy runtime (raw descriptor → normalized metadata → validator plan → ORM plan / DDL plan) means module load is cheap, migration scripts never build the ORM plan, and validator-only consumers never build the class machinery. The full reference is in docs/RIP-SCHEMA.md.
vs React / Vue / Solid
| Concept | React | Vue | Solid | Rip |
|---|---|---|---|---|
| State | useState() |
ref() |
createSignal() |
x := 0 |
| Computed | useMemo() |
computed() |
createMemo() |
x ~= y * 2 |
| Effect | useEffect() |
watch() |
createEffect() |
~> body |
Rip's reactivity is framework-agnostic — use it with React, Vue, Svelte, or vanilla JS.
Rip App
Load rip.min.js (~88KB Brotli) — the Rip compiler and Rip App framework in one file. Components are .rip source files, compiled on demand, rendered with fine-grained reactivity. No build step. No bundler.
(Rip UI is the separate widget package at packages/ui/. The two terms do not overlap.)
<script defer src="rip.min.js" data-mount="Home"></script> <script type="text/rip"> export Home = component @count := 0 render div.counter h1 "Count: #{@count}" button @click: (-> @count++), "+" button @click: (-> @count--), "-" </script>
That's it. All <script type="text/rip"> tags share scope — export makes names visible across tags. data-mount mounts the named component after all scripts execute. Two keywords (component and render) are all the language adds. Everything else (:= state, ~= computed, methods, lifecycle) is standard Rip.
Loading patterns:
<!-- Inline components + declarative mount --> <script defer src="rip.min.js" data-mount="App"></script> <script type="text/rip">export App = component ...</script> <!-- Mount from code instead of data-mount --> <script defer src="rip.min.js"></script> <script type="text/rip">export App = component ...</script> <script type="text/rip">App.mount '#app'</script> <!-- External .rip files via data-src --> <script defer src="rip.min.js" data-mount="App" data-src=" components/header.rip components/footer.rip app.rip "></script> <!-- External .rip files via separate tags --> <script defer src="rip.min.js" data-mount="App"></script> <script type="text/rip" src="components/header.rip"></script> <script type="text/rip" src="app.rip"></script> <!-- Bundle — fetch all components from a server endpoint --> <script defer src="/rip/rip.min.js" data-src="app" data-mount="App"></script> <!-- Bundle with stash persistence (sessionStorage) --> <script defer src="/rip/rip.min.js" data-src="app" data-mount="App" data-persist></script> <!-- Mix bundles and individual files --> <script defer src="/rip/rip.min.js" data-src="/rip/ui app header.rip" data-mount="App"></script>
Every component has a static mount(target) method — App.mount '#app' is shorthand for App.new().mount('#app'). Target defaults to 'body'.
The UI framework is built into rip-lang: file-based router, reactive stash, component store, and renderer. Try the demo — a complete app in one HTML file.
vs CoffeeScript
| Feature | CoffeeScript | Rip |
|---|---|---|
| Output | ES5 (var, prototypes) | ES2022 (classes, ?., ??) |
| Reactivity | None | Built-in |
| Dependencies | Multiple | Zero |
| Self-hosting | No | Yes |
Smaller codebase, modern output, built-in reactivity.
Browser
Run Rip directly in the browser — inline scripts and the console REPL both support await via the ! operator:
<script defer src="rip.min.js"></script> <script type="text/rip"> res = fetch! 'https://api.example.com/data' data = res.json! console.log data </script>
The rip() function is available in the browser console:
rip("42 * 10 + 8") // → 428 rip("(x * x for x in [1..5])") // → [1, 4, 9, 16, 25] await rip("res = fetch! 'https://api.example.com/todos/1'; res.json!") // → {id: 1, ...}
Try it live: shreeve.github.io/rip-lang
Architecture
Source -> Lexer -> emitTypes -> Parser -> S-Expressions -> Codegen -> JavaScript
(types.js) ["=", "x", 42] + source map
Simple arrays (with .loc) instead of AST node classes. The compiler is self-hosting — bun run parser rebuilds from source. The implementation lives across a handful of focused modules under src/ — lexer, compiler, schema, types, typecheck, components, parser, browser, REPL — plus the grammar sources under src/grammar/. Run wc -l src/*.js for current sizes.
The Rip Stack
Rip includes optional packages for full-stack development:
| Package | Purpose |
|---|---|
| rip-lang | Core language compiler |
| @rip-lang/server | Multi-worker app server (web framework, hot reload, HTTPS, mDNS) |
| @rip-lang/db | DuckDB server with official UI + ActiveRecord-style client |
| @rip-lang/ui | Unified UI system — browser widgets, email components, shared helpers, Tailwind integration |
| @rip-lang/swarm | Parallel job runner with worker pool |
| @rip-lang/csv | CSV parser + writer |
| @rip-lang/time | Immutable date/time with IANA timezones + Duration (US-English, zero runtime deps) |
| VS Code Extension | Syntax highlighting, type intelligence, source maps |
bun add -g @rip-lang/db # Installs everything (rip-lang + server + db)Implicit Commas
Rip rescues what would be invalid syntax and gives it elegant meaning. When a literal value is followed directly by an arrow function, Rip inserts the comma for you:
# Clean route handlers (no comma needed!) get '/users' -> User.all! get '/users/:id' -> User.find params.id post '/users' -> User.create body # Works with all literal types handle 404 -> { error: 'Not found' } match /^\/api/ -> { version: 'v1' } check true -> enable()
This works because '/users' -> was previously a syntax error — there's no valid interpretation. Rip detects this pattern and transforms it into '/users', ->, giving dead syntax a beautiful new life.
Supported literals: strings, numbers, regex, booleans, null, undefined, arrays, objects
Quick Reference
rip # REPL rip file.rip # Run rip -c file.rip # Compile rip -t file.rip # Tokens rip -s file.rip # S-expressions bun run test:all # full test suite bun run parser # Rebuild parser bun run build # Build browser bundle
Release
# rip-lang + changed @rip-lang/* packages bun run bump # Explicit version level bun run bump patch bun run bump minor bun run bump major
bun run bumpis the standard release flow for the npm ecosystem in this repo.- It bumps
rip-lang, bumps any changed publishable@rip-lang/*packages, runs the build and test steps, then commits, pushes, and publishes. packages/vscodeis intentionally excluded and must be versioned and published separately.
Documentation
| Guide | Description |
|---|---|
| docs/RIP-LANG.md | Full language reference (syntax, operators, reactivity, types, components) |
| docs/RIP-TYPES.md | Type system specification |
| docs/RIP-SCHEMA.md | Schema keyword — validators, models, ORM, DDL, algebra |
| docs/RIP-DUCKDB.md | DuckDB foreign-key constraints — what works, what doesn't, how the ORM keeps you safe |
| docs/RIP-APP.md | Rip App — browser framework architecture, subsystems, lifecycle invariants, gotchas |
| AGENTS.md | AI agents — compiler architecture, subsystems, conventions |
Zero Dependencies
Everything included: compiler, parser generator, REPL, browser bundle, test framework.
Philosophy
Simplicity scales.
Simple IR (S-expressions), clear pipeline (lex -> parse -> generate), minimal code, comprehensive tests.
Inspired by: CoffeeScript, Lisp, Ruby | Powered by: Bun
MIT License
Start simple. Build incrementally. Ship elegantly.
