Previous: , Up: Introduction   [Contents][Index]


1.3 Tutorial

Let’s compile some simple Scheme programs and learn how to work with their compiled WASM forms.

As we all know, the answer to everything is simply 42. So, we should make sure that we can compile 42 to WASM. To do so, import the (hoot compile) module and call the compile procedure.

scheme@(guile-user)> ,use (hoot compile)
scheme@(guile-user)> (define the-answer (compile 42))

The result is a WASM module. There is a lot of stuff inside, but we’re not going to focus on that right now. We should load and run the module to verify that it outputs 42 like we expect. We can do so from the comfort of our Guile REPL because Hoot includes a WASM interpreter. There’s no need to use a web browser or other WASM runtime to try out small programs.

In order to extract Scheme values from a WASM module, we actually need a second WASM module known as reflect.wasm. This is a special binary that comes with Hoot that allows for a WASM host (Guile, a web browser, etc.) to make sense of and manipulate Scheme values. To load that binary from disk we will use the (wasm parse) module.

scheme@(guile-user)> ,use (wasm parse)
scheme@(guile-user)> (define reflector
                       (call-with-input-file "js-runtime/reflect.wasm"
                         parse-wasm))

The file name js-runtime/reflect.wasm assumes you are currently located in the Hoot build directory. Adjust as necessary for your context.

Now that the reflection module has been loaded, we can load the-answer into the WASM interpreter to produce a WASM instance:

scheme@(guile-user)> ,use (hoot reflect)
scheme@(guile-user)> (define instance (hoot-instantiate reflector the-answer))

All that’s left to do now is execute the program with hoot-load:

scheme@(guile-user)> (hoot-load instance)
$5 = 42

Ta-da! It feels kind of funny to compile a Scheme program to WASM only to load it back into Scheme, but it’s a quick and easy way to test things out.

For cases when you simply want to compile an expression and see the result immediately, there is a faster method. Just use the compile-value procedure instead:

scheme@(guile-user)> (compile-value reflector '(list 1 2 3))
$6 = #<hoot (1 2 3)>

With compile-value, the compiled WASM module is thrown away, which is just fine for testing throwaway code.

Lists are cool and 42 is ultimately the answer to everything, but it would be a shame if we didn’t talk about compiling something a little more complicated. Let’s compile a simple, tail-recursive procedure! How about good ol’ factorial?

scheme@(guile-user)> (define hoot-factorial
                       (compile-value reflect-wasm
                                      '(let ()
                                         (define (factorial x result)
                                           (if (= x 1)
                                               result
                                               (factorial (- x 1)
                                                          (* result x))))
                                         factorial)))

A Hoot procedure can be called just like a regular procedure:

scheme@(guile-user)> (hoot-factorial 5 1)
$7 = 120

The Hoot reflection in Guile is great for quickly iterating on code, but what we really want is to get our programs running in a web browser. We’ve compiled a couple of things to WASM now, but the resulting modules have stayed within the confines of the Guile process. To make something that can be loaded by a web browser, we need to use the assembler to create a WASM binary:

scheme@(guile-user)> (define hello (compile "Hello, world!"))
scheme@(guile-user)> ,use (wasm assemble)
scheme@(guile-user)> (define bin (assemble-wasm hello))

Now, create a new directory for this tutorial:

mkdir hoot-tutorial

Write the binary to disk in that directory:

scheme@(guile-user)> ,use (ice-9 binary-ports)
scheme@(guile-user)> (call-with-output-file "/path/to/hoot-tutorial/hello.wasm"
                       (lambda (port)
                         (put-bytevector port bin)))

To inspect Scheme values from JavaScript, Hoot provides the js-runtime/reflect.js library. Copy that file and its associated WASM helper modules, js-runtime/reflect.wasm and js-runtime/wtf8.wasm, to the hoot-tutorial directory:

cd /path/to/hoot-tutorial
cp /path/to/guile-hoot/js-runtime/reflect.js .
mkdir js-runtime
cp /path/to/guile-hoot/js-runtime/reflect.wasm js-runtime/
cp /path/to/guile-hoot/js-runtime/wtf8.wasm js-runtime/

To run hello.wasm, we need a little JavaScript glue code. Let’s call this hello.js:

async function load() {
  const [message] = await Scheme.load_main("hello.wasm", {});
  console.log(message);
}
window.addEventListener("load", load);

We also need a minimal index.html web page to bring it all together:

<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript" src="reflect.js"></script>
    <script type="text/javascript" src="hello.js"></script>
  </head>
  <body>
    Guile is a hoot!
  </body>
</html>

The file tree in hoot-tutorial should look like this:

./js-runtime
./js-runtime/wtf8.wasm
./js-runtime/reflect.wasm
./reflect.js
./hello.js
./index.html
./hello.wasm

Finally, we need a local web server to serve the files. Fortunately, Guile includes all the building blocks we need to make a minimal one for the purposes of this tutorial. Save the following to web-server.scm:

(use-modules (ice-9 binary-ports) (ice-9 format) (ice-9 match)
             (web server) (web request) (web response) (web uri))

(define (extension file)
  (match (string-split file #\.)
    (() #f)
    ((_ ... ext) ext)))

(define (mime-type file-name)
  (or (assoc-ref '(("js" . application/javascript)
                   ("html" . text/html)
                   ("wasm" . application/wasm))
                 (extension file-name))
      'text/plain))

(define (render-file file-name)
  (values `((content-type . (,(mime-type file-name))))
          (call-with-input-file file-name get-bytevector-all)))

(define (not-found path)
  (values (build-response #:code 404) (string-append "Not found: " path)))

(define (directory? file-name)
  (eq? (stat:type (stat file-name)) 'directory))

(define (serve-file path)
  (let ((f (string-append (getcwd) (uri-decode path))))
    (if (and (file-exists? f) (not (directory? f)))
        (render-file f)
        (not-found path))))

(define (handle-request request body)
  (let ((method (request-method request))
        (path (uri-path (request-uri request))))
    (format #t "~a ~a\n" method path)
    (serve-file path)))

(run-server handle-request 'http '(#:port 8080))

Start the web server like so:

guile web-server.scm

Visit http://localhost:8080/index.html in your web browser, and if it is new enough to have WASM GC enabled, you should see the text “Hello, world!” printed in the developer console.

We hope this tutorial has helped you get started with Hoot! Read on for full API documentation.


Previous: Installation, Up: Introduction   [Contents][Index]