Blog Post: Scheme in the browser: A Hoot of a tale


Scheme in the browser: A Hoot of a tale

Spritely Institute – Tue 10 October 2023

XOR gate in Scheme Wireworld

Hey there, it’s been awhile! We’re back to share some more exciting news about Guile Hoot, a WebAssembly toolchain and Scheme→WASM compiler. In our last post we demonstrated that the Guile Hoot toolchain can be used to assemble programs written in WebAssembly Text (WAT) format, which allowed us to develop for the WASM-4 fantasy console. That was pretty cool, of course, but our goal is to compile Scheme programs (of the R7RS-small dialect, for now) to WASM. We are getting ready for our first release, and we’re happy to report that we’ve made excellent progress! We can now demonstrate the Hoot compiler working end-to-end.

Wireworld part 1: Scheme

To show off the compiler, we decided to revisit our favorite cellular automaton: Wireworld. Check out our last post to learn more about what Wireworld is and how we used Hoot to build a pure WASM version of it. This time, instead of writing several hundred lines of WAT, we wrote about 70 lines of Scheme:

(let ((grid-size 40)
      (empty 0)
      (cu 1)     ; copper
      (ehead 2)  ; electron head
      (etail 3)) ; electron tail
  (define (make-grid)
    (make-bytevector (* grid-size grid-size) 0))

  (define (grid-ref grid x y)
    (bytevector-u8-ref grid (+ (* y grid-size) x)))

  (define (grid-ref/wrap grid x y)
    (bytevector-u8-ref grid (+ (* (modulo y grid-size) grid-size)
                               (modulo x grid-size))))

  (define (grid-set! grid x y t)
    (bytevector-u8-set! grid (+ (* y grid-size) x) t))

  (define (neighbors grid x y)
    (define (check x y)
      (if (= (grid-ref/wrap grid x y) ehead) 1 0))
    (+ (check (- x 1) (- y 1))
       (check x (- y 1))
       (check (+ x 1) (- y 1))
       (check (+ x 1) y)
       (check (+ x 1) (+ y 1))
       (check x (+ y 1))
       (check (- x 1) (+ y 1))
       (check (- x 1) y)))

  (define (update from to)
    (do ((y 0 (+ y 1)))
        ((= y grid-size))
      (do ((x 0 (+ x 1)))
          ((= x grid-size))
        (let* ((t (grid-ref from x y))
               (t* (cond
                    ((= t empty) empty)
                    ((= t cu)
                     (if (<= 1 (neighbors from x y) 2) ehead cu))
                    ((= t ehead) etail)
                    ((= t etail) cu))))
          (grid-set! to x y t*)))))

  (define (copy from to)
    (let ((k (bytevector-length from)))
      (do ((i 0 (+ i 1)))
          ((= i k))
        (bytevector-u8-set! to i (bytevector-u8-ref from i)))))

  (define grid-a (make-grid))
  (define grid-b (make-grid))

  (define (update-and-swap)
    (update grid-a grid-b)
    (copy grid-b grid-a))

  (define (ref x y)
    (grid-ref grid-a x y))

  (define (set x y t)
    (grid-set! grid-a x y t))

  (define (cycle x y)
    (set x y (modulo (+ (ref x y) 1) 4)))

  (values grid-size ref set cycle update-and-swap)))

After compiling and gzipping the resulting binary, we’re looking at a 28KiB download. Not too shabby! (And we’re anticipating adding a size optimization pass resembling binaryen’s wasm-opt -Oz in the future to reduce binary size even further.)

See the full post for a LOT of detail, including how to run your own!

1 Like