Ribbit: a compact R4RS Scheme compiler that targets 25 host languages
A portable, compact and extensible Scheme implementation to 25 targets. R4RS compliant.
At a glance
- What is it?
- Ribbit is a Scheme implementation that compiles to more than 25 host languages, including JavaScript, x86 assembly, C, Python and POSIX shell, with an R4RS-compliant runtime library and a REPL that fits in a few kilobytes. It suits someone who wants a tiny embeddable Scheme or a compiler to study, and it is written in Scheme itself in a single source file.
- Who is it for?
- Ribbit fits a language implementer who wants a readable single-file compiler to study, or a developer who needs a Scheme runtime small enough to embed in a host program and who can accept that the language surface is fixed at R4RS. It does not fit a production application that needs a modern Scheme with modules, records, tail-call-friendly libraries and an active community.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 23 days ago.
- What is it written in?
- Mainly Scheme, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A compiler written in Scheme, in one file
Ribbit is a portable, compact and extensible Scheme implementation that is R4RS compliant, and the implementation detail that sets it apart is that the compiler is itself Scheme.
The compiler's source code is in a single file, src/rsc.scm. Everything the project is follows from that. You clone the repository, point any of the supported Scheme interpreters at that file, and you have a working Scheme compiler that emits code in 25 different host languages.
The language surface is R4RS, and the readme names what that includes: closures, I/O, tail calls, first-class continuations and a Read Eval Print Loop. First-class continuations are the notable inclusion, since they are the part of the Scheme report that most implementations skip and that R7RS-small also leaves out. Their presence means Ribbit's runtime has to support re-entrant control flow, which is a constraint on how small the interpreter can be.
The three headline properties are compactness, portability and extensibility. On compactness, the claim is that Ribbit removes unused code and performs a specialized compression according to the source code, with a link to a paper describing the Scheme interpreter implementation inside 7KB. On portability, it currently runs on 25 different hosts, named as JavaScript, Assembly for x86, C, Python, POSIX Shell, Prolog and more. On extensibility, Ribbit can easily define new primitives that interact with any of the 25 host languages, through a markup system described in a paper linked from the readme.
That last property is the one that makes Ribbit a compiler rather than a code generator. A primitive written in the host language and made visible to Scheme is a way to extend the language downward into its host, which is the classic interpreter technique and is what makes the 25-target claim useful rather than decorative.
The primary language is Scheme, the licence is BSD-3-Clause, the default branch is main, and the last push was on 2026-09-29. The repository has no GitHub releases. There is a CITATION.md file at the top level, alongside an AUTHORS file, and the top-level entries include bench/ for benchmarks, docs/, a flake.nix and flake.lock, and src/.
Building the compiler with Gambit or Chicken
The quick start assumes you have one of four Scheme implementations installed, and Ribbit is regularly tested with each of them.
The list is Gambit v4.7.5 or later, Guile v3.0.10 or later, Chicken v5.4.0 or later and Kawa v3.1.1 or later. If you have any of them installed on your system, it should work, and if you do not, the readme points at prebuilt binaries for Gambit and at the Gambit source repository.
The interpreter route is the simplest and it is the route the readme describes first. You run your favourite Scheme interpreter with the rsc.scm file inside the src directory.
cd src
gsi rsc.scm --help # for Gambit's interpreter
csi rsc.scm --help # for Chicken's interpreter
kawa rsc.scm --help # for Kawa
guile -s rsc.scm --help # for GuileThe four commands are the same operation through four different interpreters, and --help is the way to see the option list, which the readme also mentions as the way to find options beyond the ones documented in the README itself.
If you want a compiled compiler rather than an interpreted one, make is the other route.
cd src
# for Gambit
make rsc.exe
# for Chicken Scheme
SCHEME_COMPILER=chicken make rsc.exe
# You can then run the compiler with:
./rsc.exe --helpThe SCHEME_COMPILER variable selects the Scheme implementation used to build the compiler, and the default is Gambit. So the build itself is bootstrapped in the same way as the rest of the project: a Scheme program compiled by another Scheme.
The commands are all run from inside the src directory, which matters because the compiler takes paths to the runtime library relative to the working directory, and later examples refer to lib/r4rs/repl.scm with a relative path.
Once the compiler is built, you are ready to move to the examples, and the readme's jump to those is where the interesting part starts.
The three compiler options that matter
Three options carry most of the behaviour, and the readme documents them precisely enough that you can pick a target and a library without reading anything else.
The -t option selects the host language, and it takes the extension of the target language. The named examples are js for JavaScript, asm for x86 Assembly, c for C, py for Python, hs for Haskell and pro for Prolog, plus any of the supported targets. The example set in the readme shows the full spread: asm, c, hs, js, py, clj for Clojure, lisp, pro, scm for Scheme, sh for POSIX shell, go, lua, ml for OCaml, idr for Idris, scala, zig, adb for Ada, bas for BASIC, dart, java, jl for Julia, kt for Kotlin, pl for Prolog, ps for PowerShell and wat for WebAssembly.
The -m option causes a minification of the generated program. Two constraints come with it: it requires a recent version of Gambit, and it may need additional dependencies depending on the target under minification. The readme points at the host/<host>/minify script of a specific host for details, so the per-target behaviour is defined by a script next to that host's implementation rather than by the compiler core.
The -l option selects the Scheme runtime library, located in the lib subdirectory, and the choices are worth comparing because they are the main size lever. The r4rs library adds all essential R4RS procedures and includes a REPL that is fully R4RS compliant. r4rs/tc is like r4rs but with run time type checking. min and max are minimal libraries for small scheme implementations, with max including a minimal REPL. max-tc is like min and max but with run time type checking. And define-macro is necessary for using the define-macro construct.
Two of those entries reveal a gap in R4RS itself. The define-macro construct is not part of the original report, so it ships as a separate library, and it is easy to miss that if you assume define-macro is in r4rs. The tc variants are the other half of the design, and they matter for the embedded use case: a Scheme program running inside a host program with no type checks will fail with a confusing error far from the mistake, while the type-checked variants turn that into an immediate report at the point of the bad call.
The min and max libraries are the reason the smallest REPL is possible, since they carry only what a REPL needs.
Compiling a REPL to Python, and the 6.5KB version
The usage examples are a REPL session, and the readme shows the whole loop for Python first.
$ ./rsc.exe -t py -l r4rs lib/r4rs/repl.scm -o repl.py
$ python3 repl.py
> (+ 1 2)
3
> (define handle (open-output-file "test.txt"))
0
> (display "Hello Ribbit!" handle)
0
> ^D (Ctrl-D)
$ cat test.txt
Hello Ribbit!The pattern is the same for every target: compile the REPL program from the runtime library, then run the result with that language's interpreter. The JavaScript version is two lines.
$ ./rsc.exe -t js -l r4rs lib/r4rs/repl.scm -o repl.js
$ node repl.js
> (+ 1 2)
3And the same for three more hosts at once.
$ ./rsc.exe -t asm -l r4rs lib/r4rs/repl.scm -o repl.s # (x86 assembly, need linux as it generates an ELF file)
$ ./rsc.exe -t c -l r4rs lib/r4rs/repl.scm -o repl.c
$ ./rsc.exe -t hs -l r4rs lib/r4rs/repl.scm -o repl.hsThe comment on the assembly line is a real constraint: the x86 target generates an ELF file, so it needs Linux.
Then there is the headline number. There is a make target that produces the smallest R4RS-compliant REPL, and the readme says it takes a minute.
$ make repl-asm.exe
$ ls -la repl-asm.exe
-rwxr-xr-x 1 leonard staff 6639 5 Aug 13:36 repl.exe
!!! 6.5KB !!!
$ echo '(+ 1 2)' | ./repl-asm.exe
> 3
>Six and a half kilobytes for a REPL that is R4RS compliant, which is what the 7KB interpreter claim in the feature list refers to. The mechanism behind it is the unused-code removal and source-specialised compression the readme describes, plus the minimal runtime library rather than the full r4rs one.
That number is the single most interesting fact about Ribbit, and it is also the one that needs context. A 6.5KB binary with a REPL in it is not a general-purpose Scheme; the runtime library choice, the compression and the fact that the target is x86 assembly all matter to that figure. The same REPL compiled to Python is a Python source file of a very different size.
For an embedded use case, though, it is the number that matters. A Scheme interpreter you can link into a firmware image or a bootloader changes what is possible in that environment, and the size is what makes it plausible.
Hello world in every target, and writing your own primitive
The last example is the one that best demonstrates the point of having 25 targets, because it is a single source file compiled 25 ways.
$ echo '(display "Hello from Ribbit!")' > hello.scm
$ ./rsc.exe -t pro -l max hello.scm -o hello.pro # compile it with prolog
$ swipl hello.pro # run it with swi-prolog
Hello from Ribbit!And then the same command with the target and the extension changed, over the whole list: asm, c, hs, js, py, clj, lisp, pro, scm, sh, go, lua, ml, idr, scala, zig, adb, bas, dart, java, jl, kt, pl, ps and wat.
Two of those are worth pausing on. The scm target compiles Scheme to Scheme, which is a useful reference point because you can read the output and see what the compiler does. The wat target is WebAssembly, which is a sign of how current the target list is. And the shell target means a Scheme program can be shipped as a POSIX shell script, which is an odd but genuinely useful property for a scripting environment.
The final section of the readme is about creating your own primitive in the host language, with JavaScript and C given as the examples, and the code starts with a file in the examples directory.
$ cat examples/square.scmThe Scheme source of that file is a cond-expand over host identifiers, with a Python host case that defines a primitive named square with a docstring. The cond-expand form is the mechanism: the Scheme source tests for a host feature at compile time and emits the right definition for whichever host is being targeted.
That is the extensibility story in miniature. You write a primitive once in the host language, guard it with a host condition so it only compiles where the host supports it, and the resulting primitive is callable from Scheme in exactly the same way on every target that supports it.
The caveat is the obvious one: a primitive defined against JavaScript's runtime is not automatically available on the C target, so a library of primitives is per-host work. The readme frames it as being able to define new primitives that interact with any of the 25 host languages, which is accurate in the sense that the mechanism works on any target, not in the sense that one primitive works on all of them.
Editorial conclusion
Ribbit fits a language implementer who wants a readable single-file compiler to study, or a developer who needs a Scheme runtime small enough to embed in a host program and who can accept that the language surface is fixed at R4RS. It does not fit a production application that needs a modern Scheme with modules, records, tail-call-friendly libraries and an active community. Before adopting it, check that your host language is on the supported target list and whether you need the type-checked variants of the runtime libraries, since the tc variants of r4rs and the minimal libraries are what detect mistakes early.
Frequently asked questions
How do I build the Ribbit Scheme compiler?
The compiler source is the single file src/rsc.scm. Run it with a Scheme interpreter such as gsi rsc.scm, csi rsc.scm, kawa rsc.scm or guile -s rsc.scm, or compile it with make rsc.exe using Gambit, or SCHEME_COMPILER=chicken make rsc.exe for Chicken.
Which host languages can Ribbit compile to?
More than 25, selected with the -t option using the target extension, including js, asm, c, py, hs, pro, clj, lisp, scm, sh, go, lua, ml, idr, scala, zig, adb, bas, dart, java, jl, kt, pl, ps and wat. The x86 assembly target needs Linux because it generates an ELF file.
Which Scheme runtime libraries does Ribbit provide?
The r4rs library adds all essential R4RS procedures with a fully R4RS compliant REPL, r4rs/tc is the same with run time type checking, min and max are minimal libraries with max including a minimal REPL, max-tc adds type checking, and define-macro is needed for the define-macro construct. The -l option selects between them.
How small can a Ribbit-compiled REPL be?
The readme shows a make repl-asm.exe target producing a 6.5KB binary, which is the size of the R4RS compliant REPL built for x86 assembly with the minimal runtime library and the specialised compression the project applies.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/udem-dlteam-ribbit)Was this useful?