Skip to content

Repository files navigation

In the multiverse, you can live up to your ultimate potential. We discovered a way to temporarily link your consciousness to another version of yourself, accessing all of their memories and skills.

It's called verse jumping.

— Alpha Waymond

What is this?

vrs is a personal programming runtime - a sandbox for building my “endgame” software platform.

The goal is to create a computing environment that brings me joy.

Each piece - the language, runtime, tools, and more - is designed as part of one holistic experience.

I live on a collection of personal software running on vrs every day, evolving the runtime as I go.

Inspired by systems I love - Emacs, Erlang, Unix, Plan 9, and Hypermedia - combining their powerful ideas into something that feels just right for me.

Built at the Recurse Center.

Status

🚧 Under heavy construction, in perpetuity 🚧

🐉 Here be dragons 🐉

vrs is a personal passion project, focused on play + experimentation.

To this end:

  • There are no stability guarantees. The implementation + concepts are volatile.
  • Contributions of the intellectual kind are welcome (reach me here), but contributions of code are not accepted.
  • This software has sharp edges. Be warned!

Structure

  • lyric: Embedded Lisp Dialect and Virtual Machine
  • vrsd: A runtime implementation as a system daemon
  • libvrs: The vrs library crate shared by runtime and client implementations
  • vrsctl: A thin CLI client over libvrs
  • vrsjmp: A GUI launch bar client

Init scripts and nodes

vrsd --init PATH evaluates a script inside the runtime before accepting local clients. Unlike fread, (run PATH) evaluates all of a file's top-level forms with an implicit begin, in a fresh process, and waits for that process to finish. Services created by spawn_srv! keep running.

Every daemon has an immutable string node name. It defaults to the machine's short hostname and can be set explicitly:

cargo run --bin vrsd -- --node laptop --init ./scripts/init.ll

The repository's scripts/init.ll can nonblockingly add nodes to the service registry:

(if (eq? (node_name) "home-server")
    (run "./scripts/feedbin.ll")
    (configure :nodes '("ssh://home-server")))

Use ./serve (or ./scripts/serve.sh) to start the runtime and GUI. It first installs the current vrsctl with cargo install --locked --path vrsctl --force, so Emacs and script shebangs use the matching client. ./serve dev installs a debug client for the debug runtime socket. Use ./serve headless on a node that should run the daemon and its services without launching the GUI.

Endpoints name their transport explicitly. Release builds use VRS port 8773; debug builds use 8774, keeping a persistent serve.sh runtime isolated from ordinary cargo run development. Append :PORT to either tcp://HOST or ssh://HOST to select another port explicitly. The SSH transport runs ssh -W 127.0.0.1:PORT HOST, so SSH aliases, Bonjour names, and Tailscale MagicDNS names are resolved by OpenSSH without exposing the VRS listener. The listener remains bound to localhost.

VRS keeps each link open, exchanges service snapshots and registration changes, and caches them locally. find_srv and ls_srv only query that cache; they do not contact nodes per call. The last registration observed locally wins when a service name exists on several nodes. Remote ls_srv entries include a :node string. Each side sends a heartbeat every five seconds. After fifteen seconds without a valid message, the link is closed and that node's cached services are removed. Configured outgoing links keep reconnecting every two seconds.

To run two nodes on one machine, give each daemon a distinct local socket, node name, and listener port:

cargo run --bin vrsd -- --node alpha --node-port 8773 --socket /tmp/alpha.socket
cargo run --bin vrsd -- --node beta  --node-port 8774 --socket /tmp/beta.socket

This is deliberately only service discovery and message routing. It does not restart services or guarantee singletons. Sending to a disconnected node fails immediately. Every call, whether local or remote, fails after five seconds if the service has not replied; calls are never retried automatically. A process can change its default with (call_timeout 30).


A Tour of VRS

Introduction to Lyric

The runtime runs software written in Lyric lang:

# Use `def` to define new bindings
# e.g. "hello lyric!" string to symbol `msg`
(def msg "hello lyric!")

# Raw block strings preserve quotes and backslashes. Indented multiline blocks
# drop their leading newline and common indentation.
(def script """
    printf '%s\n' "$1"
    """)

# Update bindings with `set`
(set msg "goodbye lyric!")

# Basic Primitives - integers, lists, keywords, and more
42                                # integers
:my_keyword                       # keywords start with colon (:)
true                              # booleans are `true` or `false`
(list msg 42 :my_keyword)          # create a list of values
'("a" "b" "c")                    # quote expression with '

# Function declarations use `defn!`
# Lyric is expression-oriented - last form is returned as value to caller
(defn! double (x)
    (+ x x))
    
# Call functions by using bound symbol names within parens, followed by arguments
(double 10) # => 20

# List Operations
(def l '(1 2 3))
(def first (get l 0))       # get 0th item in `l`
(def last (get l -1))       # get last item in `l`
(contains? l 3)             # check if `l` contains `3`

# Association Lists
(def item '(:title "My Title" :subtitle "My Subtitle"))
(get item :title)      # => "My Title"
(get item :subtitle)   # => "My Subtitle"

# Functions (Lambdas) are first class
(defn! with_value (x f)
    (f x))
(with_value 41 (lambda (x) (+ x 1)))  # => 42
(map '(1 2 3) (lambda (x) (+ x x)))   # => (2 4 6)

# Conditionals with `if` - equality with `eq?`
(if (eq? msg "Hello")
    "msg was hello"
    "msg was not hello")

# and flip conditions with `not?`
(if (not? false)
    "it was not true")

# Catch error with `try`. Introspect result with `err?` or `ok?`
(if (err? (try (not_a_function)))
    "failed to call not_a_function")

# Pattern match with `match`. `_` is a wildcard pattern.
(def result '(:ok "Successful data"))
(match result
    ((:ok msg) msg)
    ((:err err) (list :err err))
    (_ '(:err "Unrecognized result")))

# Destructuring bindings can be used to pattern match against forms:
(def result '(:ok "Success"))
(def (:ok status) result)      # matches :ok, binds status to string "Success"

# As a Lisp, Lyric has `eval` and `read`:
(eval (read "(+ 40 2)")) # => 42

# Quasiquote constructs code, evaluating only comma-marked holes:
(def url "https://example.com")
`(open_url ,url)                 # => (open_url "https://example.com")
(def window '(:os/window :id 42))
`(focus_window ',window)         # => (focus_window '(:os/window :id 42))
(def commands '((notify "first") (notify "second")))
`(begin ,@commands)              # splice a list of forms without running them

# and there are more builtins and symbols in environment, introspectable via `ls_env` and `help`
(ls_env)           # see all symbols defined in environment
(help recv)        # see documentation via `help`

Macros receive source forms and return code.

(defmacro unless (test & body)
  `(if ,test nil (begin ,@body)))

(unless! false (+ 20 22)) # => 42
(macroexpand_1 '(unless! false (+ 20 22)))
# => (if false nil (begin (+ 20 22)))

The marker belongs to the macro name inside the list: (unless! ...). defn! is a standard macro; inspect it with (macroexpand_1 '(defn! echo (x) x)). macroexpand_1 expands one outer call; macroexpand repeats outer expansion until the head is ordinary code. Both accept source data and return source data; quote the call to avoid running its arguments. pretty formats the returned form, and a separate eval runs it deliberately.

Macros expand each time execution reaches a call. They use their definition's lexical scope and can call ordinary helpers or perform I/O. Redefining a macro affects subsequent calls, including calls inside existing functions. macroexpand_1 and macroexpand run the transformer, including its side effects, but leave the generated code unevaluated.

See Lyric macros for scope, generated names, and expansion limits.

In Emacs, C-c C-m displays one expansion of the form at its closing parenthesis or preceding point, adding the required quote automatically; a prefix argument repeats outer expansion. For example, use it on (srv! :test :interface '()), or evaluate (macroexpand_1 '(srv! :test :interface '())). Without the quote, the service loop runs before the inspection function can be called. C-g cancels a waiting evaluation and clears its session. Evaluation and macro expansion share a persistent editor session, so earlier evaluated definitions remain available. M-x vrs-reset-session explicitly starts a fresh connection.

See Lyric quotation and code templates for nesting, literal arguments, and the distinction between insertion and splicing.

Process

Each VRS process runs a Lyric fiber in a Tokio task, with its own environment and mailbox. Async operations such as recv and sleep suspend the task while waiting. CPU-bound Lyric code is not preempted and can occupy a worker thread.

Processes communicate through messages. Spawned processes copy their bindings; closure environments can still share mutable state, so this is not complete memory isolation.

# See list of running processes in runtime
(ps)

# See this process's process_id
(self)

# Spawn a new process
(def echo_proc (spawn (lambda ()
    (def (sender msg) (recv))
    (send sender msg))))

Message Passing

Each process has a dedicated mailbox that it can poll to receive messages:

# See messages in mailbox, without blocking or consuming a message
(ls_msgs)

# Poll for new message. This blocks execution until a message is received:
(recv)

# `recv` can poll for messages matching specific patterns
(recv '(:only_poll_for_matching msg))

# A common idiom is a "service loop" - an infinite loop that recv messages and runs some function within the process:
(loop (match (recv)
    ((:event_a ev) (handle_a ev))
    ((:event_b ev) (handle_b ev))
    (_ (error "unexpected message"))))

# Sending messages is done via `(send PID MSG)`.
# Use process id from `(self)`, `(pid PID_NO)`, and `(find_srv SRV_NAME)`
(send (pid 10) :hello)
(send (self) :hello_from_self)

# Message from child back to parent
(def parent_pid (self))
(spawn (lambda ()
    (sleep 10)
    (send parent_pid :hello_from_child)))

Services - Registry, Discovery, Binding

A service is a process registered under a name, with an exported interface. spawn_srv! starts a child service and waits for registration:

(defn! echo (message) message)
(spawn_srv! :echo :interface '(echo))

(find_srv :echo)           # process ID, including its node
(info_srv :echo :interface) # => ((:echo message))
(ls_srv)                  # service names and their registration records

(bind_srv :echo)
(echo "hello")            # => "hello"

bind_srv installs message-passing stubs in the calling process. srv! serves requests in the current process instead of spawning a child. See Services for interface metadata and binding rules.

PubSub

Topics broadcast messages to subscribers on the same runtime node.

# Subscribe to :my_topic
(subscribe :my_topic)

# Publish data to :my_topic
(publish :my_topic '(:hello :world))

# Updates are received via mailbox:
(recv) # => (:topic_updated :my_topic (:hello :world))

Examples

Example: Counter Service

#!/usr/bin/env vrsctl

# Internal state in process - count
(def count 0)

# Define an interface to increment count and publish over topic
(defn! increment (n)
  (set count (+ count n))
  (publish :count count))

# Serve a counter service, with `increment` as exported interface:
(spawn_srv! :counter :interface '(increment))

Example: System Appearance Service

scripts/system_appearance.ll wraps macOS appearance settings with exec and exports toggle_darkmode as a service.


Tooling

REPL-driven workflows via vrsctl

vrsctl is a CLI client for vrs. When invoked without arguments, it launches into an interactive REPL useful for live programming and debugging:

$ vrsctl

vrs> (+ 20 22)
42

vrs> (defn! echo (x) x)
vrs> (spawn_srv! :echo :interface '(echo))
vrs> (bind_srv :echo)
vrs> (echo "hello")
"hello"

vrsctl also offers convenient interfaces and tools to support scripting and debugging - see vrsctl --help for an overview of available commands.

Results automatically use multiline formatting when stdout is a terminal, including the REPL, --command, script files/stdin, and subscriptions (--subscribe, --follow, --followclear). Lists that fit stay on one line; larger lists put each element on its own line. Keyword/value records such as those returned by (ls_srv) keep small pairs together. The default target width is 90 columns in every output mode; use --width to choose another width. A nested value that is too large to fit after its keyword starts on the next line.

vrsctl -c '(ls_srv)'                       # readable terminal output
vrsctl --width 60 -c '(ls_srv)'             # choose a target width
vrsctl --format pretty -c '(ls_srv)' | less # readable even through a pipe
vrsctl --format compact -c '(ls_srv)'       # force compact output
vrsctl --raw -c '(pretty (ls_srv) 60)'      # display the formatter's string

Redirected stdout stays compact by default. --format default selects this terminal/pipe behavior explicitly. Strings remain quoted and escaped, including embedded newlines; --raw prints only top-level strings verbatim. It does not change nested strings. --format editor echoes source and prefixes every result line with a comment, so multiline results remain safe in a Lyric transcript. All output options apply to every input mode; subscriptions take priority over redirected stdin. Explicit files cannot be combined with commands/subscriptions.

Formatting is also available inside Lyric:

(pretty (ls_srv))       # returns a string, default width 90
(pretty (ls_srv) 60)    # positive integer width
(read (pretty '(1 2 3))) # => (1 2 3)

pretty returns text without printing or changing the value. (display VALUE) keeps its compact behavior. The pretty representation of serializable values can be parsed back with read; quote syntax and string escaping are preserved. Raw block source strings become ordinary escaped string literals when printed, preserving their decoded contents. Runtime-only values such as PIDs, references, and functions retain their existing opaque display notation and do not round-trip. Width is a target in display columns: atoms are never split, and a long string, keyword/value pair, or deep nesting can exceed it. Nothing is truncated.

Emacs Integration

emacs/vrs-mode.el provides syntax highlighting, indentation, and evaluation for .ll files using built-in Emacs libraries. Add the repository's emacs directory to load-path and load the mode:

(add-to-list 'load-path "/path/to/vrs/emacs")
(require 'vrs-mode)

Evaluation sends the source to vrsctl unchanged, including raw block strings and reader prefixes. Customize vrs-vrsctl-command to set the executable path or add options such as service bindings.

  • C-c C-e evaluates the expression at its closing parenthesis or preceding point, including its quote prefix. Try (ls_srv) or (pretty (ls_srv) 60) to see readable results in the *VRS Result* buffer. Top-level strings are displayed as text.
  • C-c C-r evaluates the region; C-c C-c evaluates the buffer.
  • C-u C-c C-e and C-u C-c C-r replace source with the result and indent it in context. Replacement preserves string quotes/escapes and leaves source intact on evaluation errors. Lists are inserted as data representations; add a quote if you want to evaluate the inserted list as literal data.
  • C-u C-c C-c displays source with commented results.
  • C-c C-m inspects one macro expansion; C-u C-c C-m repeats outer expansion.
  • M-x vrs-reset-session starts a fresh connection and clears its definitions.
  • C-g aborts a waiting evaluation and clears its session, preserving source. Effects already performed are not undone.

Buffers using the same vrs-vrsctl-command share a session. Variables, functions, macros, and service bindings persist between evaluations, including after errors. Re-evaluate a changed definition to update it.

Vrsjmp's Browse Functions opens the service-function list for execution. Enter calls the selected function after collecting its arguments. When no completion provider exists, enter a Lyric expression such as "hello" or 42.

Run M-x vrs-browse-functions to open vrsjmp and insert a call at point. You can also evaluate (vrsjmp_browse_functions) with C-u C-c C-e. Search by function or service name; each row shows its service. Press Enter to insert its form with argument names as placeholders. In the Cmd-K actions menu, choose Fill arguments to select one value per argument, using the same choices as call_interactively. The last selection returns the form without executing it. Escape out of the picker cancels the evaluation and leaves the original expression intact.

The GUI must be running for automatic opening. Requests remain in the :vrsjmp service if a wakeup is missed, and are checked again on opening or reconnection. (show_gui) asks a running GUI to open through its normal begin_interaction/get_items flow.

Customize vrs-result-width (default 90) for editor results. Indentation aligns data and keyword/value lists under their opening parenthesis, uses two spaces for call bodies, and preserves raw block string contents. The result buffer uses Lyric syntax highlighting and is read-only.

Run the Emacs tests without a runtime:

emacs -Q --batch -L emacs -l vrs-mode-tests -f ert-run-tests-batch-and-exit

The terminal harness starts a separate test runtime and includes Emacs evaluation tests when emacs is installed:

cargo build --locked -p vrsctl -p vrsd
cargo test --locked -p vrsctl --test terminal

Add --release to both commands to exercise optimized binaries. Build vrsd first because it belongs to a separate Cargo package; the tests use the daemon next to Cargo's vrsctl binary, including with a custom target directory.

The terminal harness uses a temporary socket, an ephemeral node port, and an init fixture that starts a small in-memory service. It checks startup, command, file, stdin, REPL, and subscription output on pipes and pseudo-terminals, and keeps the user's runtime and REPL history intact.

About

A Personal Software Runtime inspired by Emacs, Plan 9, Erlang, Hypermedia, and Unix

Topics

Resources

Stars

407 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages