Eli Bendersky's Bob project, a suite of Scheme implementations, adds a new compiler targeting WebAssembly, using the WASM GC extension to represent high-level language features like closures and garbage collection.
Eli Bendersky's Bob project, a long-running open-source suite of Scheme implementations, has recently gained a new compiler that targets WebAssembly. This addition represents a significant step in the project's evolution, moving beyond traditional VM implementations to explore how a high-level language with complex runtime requirements can be compiled to the WebAssembly execution model. The new compiler, which lowers parsed Scheme expressions directly to WebAssembly text format, serves two primary goals: experimenting with the practical challenges of lowering a language like Scheme—which includes built-in data structures, lexical closures, and garbage collection—to WebAssembly, and gaining hands-on experience with the WASM GC extension, which was officially added to the WebAssembly specification in October 2023.
The core technical challenge lies in representing Scheme's dynamic, garbage-collected objects within WebAssembly's type system. The WASM GC extension provides a solution by allowing the definition of structured types that can be managed by the host environment's garbage collector. In the Bob compiler, Scheme objects are carefully mapped to these WASM types. For instance, a Scheme pair (a cons cell) is represented as a struct containing two nullable references to other objects: (type $PAIR (struct (field (mut (ref null eq))) (field (mut (ref null eq))))). The (ref null eq) type indicates a reference that can be null and is subject to equality comparison. This allows pairs to contain any other Scheme value, including numbers, symbols, or other pairs, enabling the construction of arbitrary list structures.
Numbers present a particular optimization. Instead of boxing every integer in a separate struct, the WASM GC specification includes the i31 type, which can represent small integers directly as references, using one bit to distinguish them from genuine object references. This avoids the overhead of heap allocation for common numeric values. Symbols, however, require a different approach. Since WebAssembly has no built-in string type, the compiler emits symbol names into a linear memory buffer and represents each symbol by a struct containing an offset and length: (type $SYMBOL (struct (field i32) (field i32))). This design also facilitates string interning; the compiler ensures that each unique symbol name is allocated only once in linear memory, and all references to that symbol point to the same offset/length pair.
Implementing built-in functions like write, which must recursively print arbitrary Scheme values, proved to be a non-trivial exercise. The challenge stems from the opacity of WASM GC references; the host environment cannot directly inspect these references, making it difficult to delegate printing logic. The alternative of implementing write in a language like C and lowering it to WASM was also problematic, as such languages lack direct support for WASM GC object models. The solution was to implement write directly in WebAssembly text format, a task that required careful handling of type checks and recursive traversal. The implementation imports only two minimal functions from the host environment: write_char and write_i32, relying on low-level character emission for all output. For example, emitting a boolean involves checking the struct field and emitting the appropriate character for #t or #f.
The resulting compiler is a compact but functional piece of code, with the WasmCompiler class totaling just over 1000 lines of source. More than half of this code consists of the WASM text snippets that define the core runtime types and builtin functions necessary for a basic Scheme implementation. The compiler integrates with the existing Bob project infrastructure, using the shared parser and expression representation to generate WebAssembly text, which can then be compiled to binary using tools like wasm-tools and executed in environments like Node.js. While the current execution setup uses Node.js, future integration with Python bindings to wasmtime is a potential direction, though such bindings do not yet support the WASM GC extension.
This project demonstrates that compiling a high-level, garbage-collected language to WebAssembly is feasible with the GC extension, though it requires careful design of object representations and runtime functions. The approach taken in Bob—using WASM's struct types for objects and linear memory for strings—provides a clear blueprint for similar efforts. The source code for the WasmCompiler is well-documented and available for review, offering practical insights into the mechanics of code emission for WebAssembly. As the WASM GC specification matures and tooling improves, such compilers may become more common, enabling broader language ecosystems to target WebAssembly without sacrificing the high-level abstractions that developers rely on.

Comments
Please log in or register to join the discussion