how it works
yorishiro is a real Linux machine whose first and only inhabitant is a Scheme interpreter, booted entirely inside your browser. Nothing is simulated in the theatrical sense: the kernel is a real kernel, the processes are real processes, and when you reboot it, it really dies.
the construction
+----------------------------------------------------------+ | your browser tab | | +----------------------------------------------------+ | | | v86 — x86 emulator compiled to WebAssembly | | | | +----------------------------------------------+ | | | | | Linux 6.12 (32-bit, built with Buildroot) | | | | | | +----------------------------------------+ | | | | | | | PID 1 = gosh (Gauche Scheme) | | | | | | | | REPL ......... /dev/ttyS0 (COM1) | | | | | | | | MIDI jack .... /dev/ttyS1 (COM2) | | | | | | | +----------------------------------------+ | | | | | +----------------------------------------------+ | | | +----------------------------------------------------+ | | ttyS0 <-> xterm.js console | | ttyS1 --> MIDI parser --> Web MIDI device / synth | +----------------------------------------------------------+
The pattern is borrowed from Nerves, the Elixir/Erlang embedded framework: use Buildroot to produce a minimal Linux where the kernel is nothing but a hardware abstraction layer, and hand PID 1 — the first process, the one whose death is the machine's death — directly to a language runtime. Nerves puts the Erlang VM there. yorishiro puts Gauche Scheme there. The construction is language-agnostic; the choice of language is the point.
The firmware is one artifact: a bzImage with the whole root filesystem embedded as an initramfs (about 13 MB). The kernel command line says console=ttyS0 rdinit=/sbin/yorishiro-init. That init is four mount lines of shell followed by exec /usr/bin/gosh — and because exec replaces the process, PID 1 is the Scheme interpreter from that moment on. It reaps zombies on SIGCHLD, ignores the signals that would kill it, and serves a REPL on the serial console. Those are the only duties Unix actually demands of an init; everything else an init system does is convention.
The browser side boots this image with v86, an x86 emulator compiled to WebAssembly, and wires the guest's serial port to an xterm.js terminal. Line editing and history live on the browser side (the guest tty runs with echo off), a watchdog covers the whole journey from download to prompt, and the second serial port — COM2, enabled with v86's uart1 option — is treated as a MIDI jack: MIDI has been a 31,250-baud serial protocol since 1983, so the machine plays music by writing bytes to /dev/ttyS1. The page parses them (running status included) and routes them to a real Web MIDI device if you have one, or to a built-in polysynth if you don't.
tutorial — programming the machine
The console is a standard Scheme REPL, except that the process evaluating your expressions holds the machine alive. Enter submits a line; Up/Down recall history. Multi-line expressions are fine — the reader waits for balanced parentheses.
1. first words
(+ 1 2)
(map (lambda (n) (* n n)) (iota 10))
(sys-getpid) ; => 1. you are the first process.2. the machine is a file tree
Everything Linux knows is readable from Scheme, because Scheme is not sandboxed here — it is the system.
(call-with-input-file "/proc/version" read-line)
(sys-uname)
(filter #/^\d+$/ (sys-readdir "/proc")) ; every process on the machine3. children, and what init owes them
PID 1 adopts every orphan. Fork a child, let it die, and note that no zombie remains — the SIGCHLD handler in the init you are talking to reaped it.
(let ((pid (sys-fork)))
(if (zero? pid)
(sys-exit 0) ; child: die at once
(format #t "child ~a was born and reaped~%" pid)))4. write a supervisor
Nerves' deepest idea is that failure is normal and restart is the recovery strategy. The whole of it fits in a definition:
(define (supervise worker restarts)
(dotimes (i restarts)
(let ((pid (sys-fork)))
(if (zero? pid)
(begin (worker i) (sys-exit 0))
(guard (e (else #f)) (sys-waitpid pid))))))
(supervise (lambda (i) (format #t "worker ~a crashed, fine~%" i)) 3)5. music
note-on, note-off, play and play-song are defined in the init. Notes are MIDI numbers (60 = middle C); play-song takes (note ms) pairs and 0 is a rest.
(play '(60 64 67 71 72) 140) ; arpeggio
(play-song '((60 400) (0 200) (67 800))) ; with rhythm and rests
(with-input-from-file "/dev/urandom" ; kernel entropy as melody
(lambda ()
(play (map (lambda (_) (+ 48 (modulo (read-byte) 25))) (iota 8)) 170)))6. change the machine while it runs
Nothing is compiled in, nothing is sacred. The prompt is a variable; the init's own source is a readable file; any definition can be replaced while the machine breathes.
(set! *prompt* "λ> ")
(define (help) (print "you are on your own now"))
(call-with-input-file "/sbin/yorishiro-init.scm"
(lambda (p) (dotimes (i 12) (print (read-line p)))))7. mortality
The root filesystem is immutable; /tmp is the only writable ground and it vanishes with the machine. Rebooting calls a real reboot(2) — which the emulated CPU does not survive. Reload the page and an identical universe boots again, with no memory of you. Whether that is the same machine is left as an exercise.
(with-output-to-file "/tmp/soul.scm"
(lambda () (write '(i was here))))
(sys-system "reboot -f") ; the endbuild it yourself
The repository has two halves. firmware/ is a Buildroot external tree: firmware/build.sh runs the whole build inside Docker and drops the bzImage into web/public/machine/. web/ is this page — strict TypeScript, Next.js, with the emulator confined behind a single adapter interface and the lifecycle modeled as a state machine (npm run check runs typecheck, lint and tests). The same firmware boots under QEMU with qemu-system-i386 -kernel bzImage -append "console=ttyS0 rdinit=/sbin/yorishiro-init" -nographic.
lineage
The idea that a Scheme can be PID 1 is proven in production by GNU Shepherd (Guile), init of Guix System since 2013. One rung below Linux sits Loko Scheme, which runs on bare metal with drivers written in Scheme — the same construction, minus the kernel. At the bottom of the lineage are the historical Lisp machines (MIT CADR, Symbolics), whose CPUs executed Lisp natively. yorishiro is not one of those; it is a machine that Lisp inhabits rather than a machine made of Lisp — which is why it is named after the vessel, not the spirit.