ris

Ris programming language aka "Rubyist in stack memory, with agents" ;-) No garbage collector (GC). Built on iyi and inspired by Goose's memory model.

ris: rubyist in stack memory with agents

Programs always run without a collector on Linux x86_64 or aarch64, or macOS arm64 (built from source; an HTTP server there takes one connection at a time). Heap allocation traps; use tape values and copy_to for retained data.

ris is a Ruby-like language whose data lives on tape memory, with no GC, built on iyi, a fork of Crystal. Source: github.com/rislang/ris.

  • Ruby: code that reads like prose; blocks, each, map, do … end.
  • Crystal: static types, nil-safety, native code through LLVM.
  • iyi: fast modular rebuilds; check, fix, lsp, mcp for agents.
  • Rust: pub, impl, closed classes, errors as return values.
  • Go: imports by URL (github.com/…), one fmt, fibers; messages through RisBoard.
  • Goose: tape memory, bump-allocated and freed at once, with no GC.

Install: paste to an agent on your machine (Claude Code, Codex, ...)

Learn and install ris. Clone https://github.com/rislang/ris to ~/src/ris and
read its LEARN.md first. Run `sh ~/src/ris/install_ris.sh` (prebuilt; needs a C
compiler); if it has none for this machine, in the clone run `make install_ris
release=1 PREFIX=$HOME/.local` (needs Crystal, LLVM 23; 20-40 min, so a long
timeout). Put ~/.local/bin on PATH; register `ris mcp` as an MCP server (Claude
Code: `claude mcp add ris -- ris mcp`). Then `ris init hello ~/src/hello`, make
it print "Hello, world!", run it, show me. End with exactly: "Congrats! Next,
ask me to build your personal blog in ris ;-)" Don't start the blog.

Why ris: tape memory, without the GC

Parse a request and build its response on one tape: bump-allocated, and every def, loop round and block frees what it made when it ends. Main and each fiber have an implicit tape; HTTP, JSON and Slim all work on it:

import std/http::{Server, ParsedHead, Field}  # supports tape memory
import std/json::{JSON}                # supports tape memory
import github.com/domify/slim::{Slim}  # supports tape memory
bytes = "GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n"
fields = Array(Field).new(16)
req = Server.parse_head(bytes, 0, true, fields) # fields view the bytes
if req.is_a?(ParsedHead)
  body = String.build do |writer| # JSON written on the tape
    writer.json { |json| json.object { json.field "path", req.target } }
  end
  puts body                             # => {"path":"/hello"}
  puts Slim.render("views/page.slim") # h1= req.target
end

A literal is a String view of static bytes; interpolation builds a String on the tape. Classes live on the tape too; there is no heap. Tape values go to a file or socket uncopied; tape blocks share one tape. The compiler refuses values outliving their scope; text.copy_to(owner) copies what must. SPEC.md.

How fast: ris-bench

respond.ri, 1.6M DTOs ris Goose C++ Rust Go Ruby²
Time 0.67 s 0.67 s 0.97 s 1.18 s 1.67 s 118 s
Memory 1.7 MB 2.0 MB 4.1 MB 2.3 MB 8.7 MB 15 MB
Heap allocated 0 0 1.7 GB 1.6 GB 1.7 GB 2.2G objects

As JSON; release, median of 9. ² YJIT, 95 s Bignum math; Spinel 127 s. Rust with bumpalo 1.03 s. Slim (Slang): 4x faster.

Campfire port, the whole app: busiest page, 40 messages, 364,500 bytes; 2026-10-17, Ryzen 7 PRO 6850U, 64 connections, three 10 s runs.

1 core req/s p99 memory 8 CPUs req/s p99 memory
ris 6,847-7,168 10-11 ms 20 MB 21,822-22,040 9-11 ms 172 MB
Rust 7,066-7,187 16 ms 52 MB 20,639-21,497 6-7 ms 70 MB
Rails¹ 444 226 ms 309 MB

¹ Other machine, gzip: approximate. ris on 8 CPUs: 8 processes, one port.

respond: tape 0.67 s
/ 1.7 MB; heap 1.55 s / 7.6 MB; 5x slower playback; a block is 0.05 MB

Status and thanks

ris 0.1.0 is a prototype; internals keep iyi's names so iyi merges. Thanks to Serdar Dogruyol (iyi), Wouter van Oortmerssen (Goose), Crystal (Ary Borenszweig, Juan Wajnerman, Brian Cardiff, Manas and community) and Ruby (Yukihiro Matsumoto). LICENSE: Apache 2.0. Windows: WSL2 for now. Developed with Claude Opus 5.5, ChatGPT Codex Sol 6.1.