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
Future Directions
One major area of future work will be in debugging support for the web app scripting use case. Experiments in using Scheme-JS to replace JavaScript in a real web app have shown how important such support is. I am in the process of building integrating the Scheme-JS debugging capabilities into Chrome dev toolsVibe Coding
It's worthwhile to note that Scheme-JS has been completely vibe coded, with almost no traditional coding. That said, I am a very experienced programmer, with considerable knowledge of Scheme and some of its implementation strategies, and I gave the AI models and agents considerable guidance in many situations. For those who are interested, I plan to have a followup article talking in detail about the vibe coding aspects of this project.Appendix
JavaScript Interoperability Features
This implementation provides deep integration between Scheme and JavaScript. See Interoperability.md in the GitHub repo for more complete technical details.
Import with: (import (scheme-js interop))
Procedure | Description | Example |
(js-eval str) | Evaluate JavaScript code | (js-eval "Math.PI") → 3.14159... |
(js-ref obj prop) | Access object property | (js-ref console "log") |
(js-set! obj prop val) | Set object property | (js-set! obj "x" 42) |
(js-invoke obj method args...) | Call object method | (js-invoke console "log" "Hi") |
(js-obj key val ...) | Create JS object | (js-obj 'x 1 'y 2) → {x: 1, y: 2} |
(js-obj-merge obj ...) | Merge objects | (js-obj-merge obj1 obj2) → {...obj1, ...obj2} |
(js-typeof val) | Get JS type | (js-typeof 42) → "number" |
js-undefined | JS undefined value | (eq? x js-undefined) |
(js-undefined? val) | Undefined predicate | (js-undefined? x) → #t |
js-null | JS null value | (eq? x js-null) |
(js-null? val) | Null predicate | (js-null? x) → #t |
(js-new constructor args...) | Instantiate JS class | (js-new Date 2024 0 1) |
Use js-new to create instances of JavaScript classes with the new operator:
;; JavaScript globals are automatically available
(define now (js-new Date))
(define birthday (js-new Date 1990 0 1))
;; Standard library classes
(define my-map (js-new Map))
(my-map.set "key" "value")
(my-map.get "key") ;; => "value"
;; Create arrays with specific length
(define arr (js-new Array 10))
arr.length ;; => 10
Dot Notation Syntax
Access JavaScript object properties using familiar dot notation:
;; Property access
(define obj (js-eval "({name: 'alice', age: 30})"))
obj.name ;; => "alice"
obj.age ;; => 30
;; Chained access
(define nested (js-eval "({a: {b: {c: 42}}})"))
nested.a.b.c ;; => 42
;; Property mutation
(set! obj.name "bob")
obj.name ;; => "bob"
;; Method call
(obj.method args) ;; => (js-invoke obj "method" args)
Under the hood:
Input | Transformed To |
obj.prop | (js-ref obj "prop") |
(obj.method arg) | (js-invoke obj "method" arg) |
(set! obj.prop val) | (js-set! obj "prop" val) |
Object Literal Syntax #{...}
Create JavaScript objects using a concise literal syntax:
;; Basic object
#{(x 1) (y 2)} ;; => {x: 1, y: 2}
;; With expressions
#{(sum (+ 1 2)) (pi 3.14)} ;; => {sum: 3, pi: 3.14}
;; Spread syntax
(define base #{(a 1) (b 2)})
#{(... base) (c 3)} ;; => {a: 1, b: 2, c: 3}
Literal Evaluation Semantics: The #{} object literal syntax evaluates its values at runtime (like JavaScript), while #() vector literals do not evaluate their contents (following R7RS standard). When nesting object literals (or any evaluated expressions) inside vectors, use (vector ...) instead of #(...) to ensure the objects are evaluated:
;; Correct - objects are evaluated:
(vector #{(x 1)} #{(y 2)})
;; => [object, object]
;; Incorrect - creates unevaluated expressions:
#(#{(x 1)} #{(y 2)})
;; => [cons-cell, cons-cell]
Promise Library
Import with: (import (scheme-js promise))
Provides transparent interoperability with JavaScript Promises.
Procedure | Description |
(js-promise? obj) | Returns #t if obj is a Promise |
(make-js-promise executor) | Create Promise with (lambda (resolve reject) ...) |
(js-promise-resolve value) | Create resolved Promise |
(js-promise-reject reason) | Create rejected Promise |
(js-promise-then p handler) | Attach fulfillment handler |
(js-promise-catch p handler) | Attach rejection handler |
(js-promise-finally p thunk) | Attach cleanup handler |
(js-promise-all list) | Wait for all promises |
(js-promise-race list) | Wait for first to settle |
(js-promise-all-settled list) | Wait for all to settle |
(js-promise-map f p) | Apply function to resolved value |
(js-promise-chain p f ...) | Chain promise-returning functions |
(async-lambda formals body ...) | Macro for CPS transformation |
(import (scheme-js promise))
(define p (js-promise-resolve 42))
(js-promise-then p
(lambda (x)
(display x)
(newline)))
;; Chain promises
(js-promise-chain (fetch-url "http://example.com")
(lambda (response) (parse-json response))
(lambda (data) (process data)))
Footnotes:
[1] It was originally developed at Google[2] See, for example, https://en.wikipedia.org/wiki/Structure and Interpretation of Computer Programs
[3] Pettyjohn, Greg, John Clements, Joe Marshall, Shriram Krishnamurthi and Matthias Felleisen. “Continuations from generalized stack inspection.” ACM SIGPLAN International Conference on Functional Programming (2005).
[4] Marshall, Joseph. "An Unexceptional Implementation of First-Class Continuations." In Proceedings of the 2009 International Lisp Conference, pp. 36-40. 2009.
[5] Marshal, Joseph, "A Method for Implementing First-Class Continuations on the JVM and CLR (AI assisted),, Abstract Heresies(blog), https://funcall.blogspot.com/2025/10/a-method-for-implementing-first-class.html
[6] Thanks, Joe!
[7] The vibe coding was VERY guided. I had a pretty clear idea of what I wanted and enough knowledge of Scheme and its implementations to guide it pretty carefully. A future article will go into more details on the vibe coding process for Scheme-JS

No comments:
Post a Comment