Haunt and Hoot and Blocks oh my!

This post coincides with the release of 0.2.0 of Guile Blocks, an Org Mode inspired implementation of source blocks in Guile Scheme. This post demonstrates some of the functionality I've been working on. You can find the code for this post here.

I previously wrote another post showcasing more new functionality. You can find that post here.

Hoot

Hoot is now supported! Hoot is a project by the Spritely Institute used to run Scheme code in WebAssembly (Wasm) GC-capable web browsers. From the perspective of a blogger, using Hoot works like this:

  1. Write Scheme code that performs some kind of calculation.
  2. Add Wasm imports to manipulate things outside the Wasm environment.
  3. Load the Wasm module using a JavaScript shim.

That second item is where the magic happens. Imports are JavaScript functions, and can take and return values. These values can be common types like integers or strings, but also more complicated types like Scheme procedures!

In the below example, I have a Hoot block that uses two imports, one to insert text into an HTML element and one to run a Guile function on a timer.

(use-modules (guile)
             (hoot ffi)
             (ice-9 match))

(define *rotation* 0)

(define pi 3.141592654)

(define distance-max 2.0)

(define (inside-donut? coordinate)
  (match coordinate
    ((x y) (and (<= (+ (expt x 2) (expt y 2)) 1.0)
                (>= (+ (expt x 2) (expt y 2)) 0.1)))
    (e (error "Invalid coordinate" e))))

(define (coordinate->distance coordinate degrees)
  (match coordinate
    ((x y)
     (let* ((radians (* degrees pi (/ 1 180)))
            (shape-coordinate (list (/ x (cos radians)) y)))
       (if (not (inside-donut? shape-coordinate))
           (nan)
           (1+  (* (car shape-coordinate)
                   (sin radians))))))
    (e (error "Invalid coordinates" e))))

(define (distance->ascii distance)
  (define asciiscale
    ".'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxzXYUQ0OZmwqhao*#MW&8%B@$")

  (if (nan? distance) #\space
      (string-ref asciiscale
                  ;; at distance=distance-max, we'd get (string-ref a
                  ;; (length a)), which would cause an error.
                  (min (1- (string-length asciiscale))
                       (inexact->exact (round (* (string-length asciiscale)
                                                 (/ distance distance-max))))))))

(define (coordinate->ascii coordinate rotation)
  (distance->ascii (coordinate->distance coordinate rotation)))

(define (generate-coordinates resolution)
  "Generate an equidistant list of coordinates of length @var{resolution}
between -1 and 1."
  (when (even? resolution)
    (error "Resolution must be odd." resolution))
  ;; e.g. 5->'(-1 -0.5 0 0.5 1)
  (let ((point-vals (iota (1+ resolution)
                          -1
                          ;; -1 + x*resolution = 1
                          ;; x = 2 / resolution
                          (/ 2 resolution))))
    ;; '(((-1 -1) (-1 -0.5) ...) ((-0.5 -1) ...) ...)
    (map (lambda (x)
           (map (lambda (y)
                  (list x y))
                point-vals))
         point-vals)))

(define (generate-circle resolution rotation)
  (string-join
   (map (lambda (row)
          (list->string
           (map (lambda (coordinate)
                  (coordinate->ascii coordinate rotation))
                row)))
        (generate-coordinates resolution))
   "\n"))

(define-foreign display-circle
  "my_dom" "set_element"
  (ref string) -> (ref null extern))

(define-foreign add-timer
  "my_control" "registerTimer"
  i32 (ref null extern) -> (ref null extern))

(define (update-circle)
  (let ((resolution 51))
    (set! *rotation* (if (> *rotation* 360)
                         0
                         (+ *rotation* 3)))
    (display-circle (generate-circle resolution *rotation*))))

(let ((delay-in-ms 50))
  (add-timer delay-in-ms (procedure->external update-circle)))

For those who want to set up a Hoot+Haunt environment of their own, be aware that Hoot only works on "bleeding edge" versions of Guile. If you're using Guix to set up the environment, be sure that Haunt is configured to use guile-next and not guile. Otherwise you may encounter build failures depending on how exactly you're invoking Hoot.

Here's an example of how to do that.

(define (guile->guile-next base)
  (package
    (inherit base)
    (inputs (modify-inputs (package-inputs base)
              (replace "guile" guile-next)))))

(guile->guile-next haunt)
;;=> a variant of haunt that uses guile-next

There's definitely more work to be done. For one, the JavaScript wrappers aren't handled in the most elegant way, and the scripts themselves are completely inline (as opposed to written to a file). This isn't hard for a user to fix, but it isn't great either.

It would also be nice to support more Wasm runtimes, such as Ruby or Rust. This may result in some backwards-incompatible changes to Guile Blocks's Hoot support as things are refactored.

Info!

Guile Blocks now has an info manual! An online mirror of the manual is hosted at guile-blocks.info. Forgive the rough edges, the pipeline to convert Org to TexInfo to both Info and HTML is rough.

Ngspice

Guile Blocks now supports Ngspice! As always, it is possible pipe the output from Ngspice to graphing programs like Gnuplot to generate graphs.

.title dual rc ladder
R1 int in 10k
V1 in 0 dc 0 ac 1 PULSE (0 5 1u 1u 1u 1 1)
R2 out int 1k
C1 int 0 1u
C2 out 0 100n
.control
ac dec 10 1 100k
wrdata /dev/stdout vdb(out)
.endc
.end