Skip to content

shreeve/rip-lang

Repository files navigation

Rip Logo

Rip

A modern language that compiles to JavaScript

Version Dependencies Tests License


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-hostingbun 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

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
!? (otherwise) val !? 5 Default only if undefined (infix)
!? (defined) val!? True if not undefined (postfix)
?! (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

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
///

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 UI

Load rip.min.js (~54KB Brotli) — the Rip compiler and UI framework in one file. Components are .rip source files, compiled on demand, rendered with fine-grained reactivity. No build step. No bundler.

<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="bundle" data-mount="App"></script>

<!-- Bundle with stash persistence (sessionStorage) -->
<script defer src="/rip/rip.min.js" data-src="bundle" data-mount="App" data-persist></script>

<!-- Mix bundles and individual files -->
<script defer src="/rip/rip.min.js" data-src="/rip/ui bundle 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
Lexer 3,558 LOC 2,024 LOC
Compiler 10,346 LOC 3,293 LOC
Total 17,760 LOC ~11,890 LOC

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
           (1,778)    (types.js)     (359)       ["=", "x", 42]     (3,334)      + source map

Simple arrays (with .loc) instead of AST node classes. The compiler is self-hosting — bun run parser rebuilds from source.

Component File Lines
Lexer + Rewriter src/lexer.js 1,778
Compiler + Codegen src/compiler.js 3,334
Type System src/types.js 1,091
Component System src/components.js 2,026
Source Maps src/sourcemaps.js 189
Type Checking src/typecheck.js 442
Parser (generated) src/parser.js 359
Grammar src/grammar/grammar.rip 948
Parser Generator src/grammar/solar.rip 929
REPL src/repl.js 600
Browser Entry src/browser.js 194
Total ~11,890

The Rip Stack

Rip includes optional packages for full-stack development:

Package Version Purpose
rip-lang 3.13.62 Core language compiler
@rip-lang/server 1.3.12 Multi-worker app server (web framework, hot reload, HTTPS, mDNS)
@rip-lang/db 1.3.15 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 1.2.18 Parallel job runner with worker pool
@rip-lang/csv 1.3.6 CSV parser + writer
@rip-lang/schema 0.3.8 Unified schema → TypeScript types, SQL DDL, validation, ORM
VS Code Extension 0.5.7 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           # 1436 tests
bun run parser         # Rebuild parser
bun run build          # Build browser bundle

Release

# rip-lang + changed @rip-lang/* packages + @rip-lang/all
bun run bump

# Explicit version level
bun run bump patch
bun run bump minor
bun run bump major
  • bun run bump is the standard release flow for the npm ecosystem in this repo.
  • It bumps rip-lang, bumps any changed publishable @rip-lang/* packages, updates @rip-lang/all, runs the build and test steps, then commits, pushes, and publishes.
  • @rip-lang/all is released automatically as part of that flow; there is no separate manual release step for it.
  • packages/vscode is 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
AGENTS.md Compiler architecture, S-expressions, component system internals
AGENTS.md AI agents — get up to speed for working on the compiler

Zero Dependencies

{ "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.