Rip 语言。编译到 ES2022。内置响应式。
Rip language. Compiles to ES2022. Built-in reactivity

原始链接: https://github.com/shreeve/rip-lang

## Rip:一种现代 JavaScript 语言 Rip 是一种新的、简洁的语言,编译为 ES2022 JavaScript,灵感来自 CoffeeScript。它拥有较小的代码库(约 11,000 行代码),没有依赖项,并且使用 Bun 构建了一个自托管编译器。 主要特性包括一打新的运算符(例如 `!` 用于 `await`,` =~` 用于正则表达式匹配),使用 `:=` 和 `~=` 等运算符进行内置的响应式编程,以及可选的类型注释。Rip 通过 Ruby 风格的构造函数、列表推导式和简化的语法来简化开发——消除了导入、钩子和依赖数组。 除了核心语言之外,Rip 还提供用于服务器端开发、数据库交互(与 DuckDB)、UI 组件等的包。一种独特的“Rip Schema”为数据验证、ORM 和数据库迁移提供了一种统一的方法。 Rip 旨在易于使用,提供基于浏览器的 REPL、VS Code 扩展,并专注于简单性和增量开发。它旨在成为一种强大而易于接近的语言,适用于前端和后端项目。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Rip 语言。编译到 ES2022。内置响应式 (github.com/shreeve) 8 分,perfunctory 发表于 1 小时前 | 隐藏 | 过去 | 收藏 | 讨论 帮助 考虑申请 YC 2026 夏季项目!申请截止至 5 月 4 日 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系方式 搜索:
相关文章

原文

Rip Logo

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

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

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:

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

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

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)
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
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"

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

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

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.

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.


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

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


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.


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.


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.


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


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.


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)

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


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

# 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 bump is 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/vscode is intentionally excluded and must be versioned and published separately.

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
AGENTS.md AI agents — compiler architecture, subsystems, conventions

Everything included: compiler, parser generator, REPL, browser bundle, test framework.


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.

联系我们 contact @ memedata.com