An implementation of the Scheme R7RS-Small standard in JavaScript, designed for deep JavaScript interoperability.
Implementation Highlights
- R7RS-Small: Fully conforms to the R7RS-small standard.
- Tail Call Optimization (TCO): Proper tail recursion by the interpreter (though Interleaved JS and Scheme code may still cause stack overflow.)
- First-Class Continuations: Full support for call/cc.
- JavaScript Interop: Seamless calling between Scheme and JavaScript, including shared data representation and transparent boundary crossing. Scheme closures and continuations are first-class JavaScript functions. JavaScript global definitions are automatically visible in the Scheme global environment.
- Browser Scripting: Replace JavaScript in web apps with <script type="text/scheme"> tags. Direct evaluation of Scheme by JavaScript is also supported.
- Node.js REPL: Full-featured interactive REPL with history, multiline support, and parenthesis matching.
- Browser REPL: Interactive browser-based REPL via a custom <scheme-repl> web component.
- Debugger: Basic debugging support in the REPLs for breakpoints, stack navigation, and stepping.
Background and History
My background is in programming language design and implementation, and I was deeply immersed in the Scheme world back in the 1990's, especially in my time as a Research Scientist at the MIT A.I. lab working on the MIT Scheme system. Fast forward about 20 years, when I cofounded what is now MIT App Inventor[1] and used Scheme to represent its core intermediate language and implement its runtime environment.
For a while now I've been working, on and off, on a visual programming system that would allow people to build web-based apps. It's mostly meant as a tool for teaching kids (of all ages) programming. It uses a "blocks-based" UI, similar in spirit to App Inventor, Scratch and Snap!. Like App Inventor, the users should be able to build "real", standalone apps. Like Snap!, I want it to be built on a solid semantic and pedagogical base. The Scheme programming language has a long history as such a base[2], so I want the building blocks of my visual language to be based on Scheme.
Consequently, I've been looking for an implementation of the Scheme programming language that had the following:
- A full r7rs-small Scheme, including a numeric tower, proper tail recursion and call/cc.
- Maximal interoperability with JavaScript (in the browser and in node.js).
At some point I realized that I probably was going to have to create my own implementation of Scheme to meet my needs, but I knew that it would be a difficult and slow endeavor, so I kept putting it off. However, a few things happened relatively recently that gave me some hope. One was that I discovered some research into implementing call/cc in ways that could be implemented by or interoperate with other high-level programming languages. These papers were:
- Continuations from Generalized Stack Inspection[3]
- An Unexceptional Implementation of First-Class Continuations[4]
- A Method for Implementing First-Class Continuations on the JVM and CLR (AI assisted)[5]
I also had some personal communication with Joe Marshall[6] (the author or co-author of the above papers), which gave me some more confidence.
Around the same time I was aware of the increasing capabilities of A.I. assisted programming. I'm a good programmer, I think, but I'm a slow programmer, and the hope that A.I. could accelerate the process energized me. I was also hoping that A.I. could help in understanding the research mentioned above and applying it to JavaScript.
The above confluence of events led me to try and vibe code[7] my way to a new Scheme implementation. It's now complete (of course it may have bugs) with respect to the features mentioned at the top of this article. I should mention, though, that performance was a non-goal for this implementation. In my intended use cases I am hoping that performance won't be an issue.
You can try out the browser-based REPL here. The GitHub repo is here.
Feature Details
Let's go into a little more detail on the features that I mentioned in the highlights at the top of this article.
R7RS-SmallScheme-JS is (modulo any bugs) a conformant implementation of Scheme as described in the Scheme R7RS-small standard. It passes what I call the "Chibi compliance tests", which is a fairly comprehensive test suite originally developed for Chibi Scheme. Additionally, we created a test suite which consists of all the examples in the R7RS-standard document.
As a standard-conforming Scheme, Scheme-JS is properly tail recursive (though Interleaved JS and Scheme code may still cause stack overflow.) It also has full support for first-class continuations (i.e. call/cc), in a language which itself supports neither. That was where the research mentioned above came in.
JavaScript Interoperability
There are three elements of JavaScript interoperability in this implementation of Scheme: JS transparency, JS-like syntax and exposing JS operations.
The first element, JS transparency, has to do with being able to call into JavaScript from Scheme, and vice versa, with the least effort on the part of the user who is coding with Scheme-JS. It also means that Scheme data types are "transparently" converted to (or represented as) JavaScript data types, and vice versa, as we cross the JS<->Scheme boundary. Consequently, Scheme vectors are JavaScript arrays, Scheme records are JavaScript objects, Scheme procedures and continuations are directly callable by JavaScript functions.
In order to support a complete Scheme numeric tower and proper exactness, Scheme-JS implements exact numbers using JavaScript BigInts and provides, by default, automatic deep conversion when passed to native JavaScript functions. This obviously has some performance implications, but benchmarking seems to show that the slowdown (over representing all numbers as JS Numbers and having no exact numbers in the numeric tower) is on the order of 10%
The second element is about providing convenient JS-like syntax in Scheme. In Scheme-JS this involved adding a dot notion syntax for accessing and setting JS object properties and adding a syntax for creating JS object literals.
The third element has to do with giving explicit Scheme access to primitive JavaScript operations. Scheme-JS supports that with a set of Scheme procedures. Related to this, JavaScript global definitions (on window or globalThis) are automatically visible in the Scheme global environment.
Some more details about the JavaScript interoperability can be seen in the Appendix.
Node.js REPL
Scheme-JS has a node-based interactive REPL with history, multiline support, and parenthesis matching.
Browser REPL
Scheme-JS has a browser-based REPL which can be embedded in an HTML page via a custom <scheme-repl> web component.
Browser Scripting
You can use Scheme-JS to replace JavaScript in web apps by using <script type="text/scheme"> tags. You can also directly access the Scheme-JS evaluator functions from JavaScript.
Debugger
Debugging commands include:
- :debug on|off - Enable/disable debugging
- :break <file> <l> [c] - Set breakpoint
- :unbreak <id> - Remove breakpoint
- :breakpoints - List all breakpoints
- :step / :s - Step into
- :next / :n - Step over
- :finish / :fin - Step out
- :continue / :c - Resume execution
- :bt / :backtrace - Show backtrace
- :locals - Show local variables
- :eval <expr> - Evaluate in selected frame's scope
- :up / :down - Navigate stack frames
- :help - Show help
