Everything beside this is live rendered by Goeteia.
The Scheme below is compiled to WebAssembly in your browser and mounted live.

booting the compiler…

No server compiles this — the page carries the whole compiler (goeteia.wasm, ~38 KB gzipped, cached after first load), and each Run recompiles the source above in ~15 ms.

01 · The language

R6RS and syntax-case, complete — in the page

Not a toy subset: hygienic syntax-rules and procedural syntax-case with fenders, nested ellipses and datum->syntax, compiled to Wasm GC right here in your browser.

The whole standard, running above

Exact bignums and rationals, call/cc and dynamic-wind on Wasm's own exception handling, and every tail call a return_call — a hundred-million-iteration loop runs in constant stack.

The expander is a compile-time interpreter with hygiene by renaming: a macro's bindings and yours can never collide. Paste any of this into the editor above and press Run.

(define-syntax while              ; syntax-rules
  (syntax-rules ()
    ((_ c body ...)
     (let loop ()
       (when c body ... (loop))))))

(define-syntax inc!               ; procedural syntax-case
  (lambda (x)
    (syntax-case x ()
      ((_ v)   #'(set! v (+ v 1)))
      ((_ v n) #'(set! v (+ v n))))))

(fact 20)  ; => 2432902008176640000 -- exact
(/ 1 3)    ; => 1/3 -- a rational, not 0.333…
02 · The web as list

Macros expand into HTML and CSS

A document is a tree; a stylesheet is a list of rules — the exact shapes s-expressions were made for. (web html) and (web css) are two pure functions over one representation, and (web sx) is a macro over it.

The page you're reading is the proof

Every element and every CSS rule of this site expands from Scheme — site/index.ss is the whole page; the "Built in pure Scheme" badge shows you the source. A colour is one binding shared by styles and code; DRY is append.

The sx template macro splits at expansion time: the static tree is built once, each unquote becomes a hole wired to a signal — one text node updates, and nothing re-renders. No virtual DOM, no diffing.

(define (card title . body)       ; UI is a function
  `(div (@ (class "card"))
     (h3 ,title) (p ,@body)))

(css->string                       ; CSS is a list
 `((.card (background (var bg2))
          (border-radius (px 12)))))
;; => ".card{background:var(--bg2);border-radius:12px}"

(sx (button (@ (on-click ,bump!))  ; a macro: the static
      "clicked " ,(signal-ref n)))  ; tree is built ONCE,
                                    ; holes become effects
03 · Text & the DOM

Typesetting without a layout engine

(web dom) wraps the browser in ordinary procedures. (web typeset) — after pretext — takes the layout engine out of the browser: measure each distinct code point once, then layout is a pure function from metrics to line boxes.

Layout you can compute, not await

Heights are known before anything touches the DOM — virtual scrolls and streaming chat stop guessing — and text can be set where no layout engine exists at all: canvas and WebGL scenes. Greedy first-fit breaking with CJK kinsoku — closing punctuation never starts a line.

It is at work on this site: the hero's subtitle and the Why Scheme? page's headings are set glyph by glyph by (web typeset) — which is why they can dodge your cursor.

;; (web dom): the browser, as ordinary calls
(define el (create-element "span"))
(set-text! el "the glyph")
(append-child! (get-element-by-id "live") el)

;; (web typeset), after pretext: measure once,
;; then layout is pure arithmetic -- no DOM
(define l
  (layout (prepare text (canvas-measurer font))
          max-width line-height))

(layout-height l)   ; known BEFORE anything renders
(for-each place-line! (layout-lines l))
04 · Graphics

3D, from s-expressions

(gfx gl) drives WebGL 2 through a command buffer — shadow maps, PBR, HDR bloom, SSAO, instancing, skeletal animation from glTF — and (gfx glsl) renders shaders written as s-expressions to either GLSL dialect.

This exact program runs above

It is the sky of the skybox.ss tab in the live editor — switch to it, edit a form, press Run. The mirror floor of pointlight.ss and the WebGPU fire of particles.ss — a hundred thousand particles whose physics is a compute shader — are tabs beside it.

Because a shader is a datum, macros can write shaders: the hero's twelve thousand particles run their physics in a vertex shader assembled by Scheme.

(define sky-p
  (fx-program!
   '((attribute vec3 a_pos)
     (uniform mat4 u_vp)
     (varying vec3 v_dir)
     (define (main) void
       (set! v_dir a_pos)
       (local vec4 p (* u_vp (vec4 a_pos (fl 0))))
       (set! gl_Position p.xyww)))  ; the sky never moves
   '((precision mediump float)
     (uniform samplerCube u_sky)
     (varying vec3 v_dir)
     (define (main) void
       (set! gl_FragColor (textureCube u_sky v_dir))))))
05 · Networking

S-expressions on the wire, continuations over it

When the backend is also Scheme (Igropyr), requests and replies are s-expressions — there is no protocol to design, read and write are the codec. And with continuations on the server, a whole multi-request dialogue is ordinary control flow.

The dialogue is a process

A wizard, a booking, a transfer — the flow runs as one process whose local bindings are the conversation state. “The user is at the confirm step” means the process is parked at that line — a step order the code cannot express cannot happen.

Death for any reason answers gone — proof the transaction rolled back. On this side it is all (web rpc): datum in, datum out, exact rationals intact; (web ws) and (web sse) push datum streams; (web json) covers every other backend.

;; (web rpc): the wire carries a datum
(rpc "/rpc" '(add 1 2 1/2))     ; => (ok 7/2) -- exact
(ws-connect! "/live" (lambda (msg) ...))

;; the server (Igropyr): a dialogue is ONE process,
;; parked at a line by its continuation
(conversation-start!
  (lambda (req suspend!)
    (let ((req2 (suspend! confirm-page)))  ; round-trip;
      (commit!)                            ; resumes HERE
      done))
  req)

(conversation-resume! id req2)  ; => reply | 'gone
;; 'gone means: rolled back. guaranteed.

What's inside

Self-hosting, to the byte

The compiler is written in the Scheme subset it compiles. The self-hosted build recompiles itself and the output is byte-identical — the fixpoint is checked in CI fashion on every change, and every test runs through both stages.

Native Wasm GC objects

Fixnums are unboxed i31refs, pairs and records are GC structs, eq? is one ref.eq. No shadow heap in JavaScript: the host supplies two byte-stream imports and nothing else.

Hygienic macros

syntax-rules and procedural syntax-case with fenders, nested ellipses and datum->syntax, running in a compile-time interpreter with hygiene by renaming.

Real closures, real tail calls

Typed function references with a fast per-arity entry and a generic entry per closure — variadic procedures and apply are cheap, and every tail call is a return_call. A 100M-iteration loop runs in constant stack, in ~150ms.

call/cc & dynamic-wind

Escape continuations ride the Wasm exception-handling proposal: capture is O(1), the normal path costs one try block, and winders unwind inner-to-outer on the way out.

A reactive web stack

(web sx) templates over fine-grained (web reactive) signals, an (web html) renderer, and a (web js) FFI that reaches straight into the host — this page is built with it.

3D and WebGL

(gfx gl) drives WebGL 2 through a command buffer with shaders as s-expressions in (gfx glsl), rendered to either GLSL dialect from the same forms. Shadow maps, PBR, HDR bloom, SSAO, instancing, skeletal animation from (gfx gltf) assets, and transform-feedback particles whose physics is the vertex shader — the title above is twelve thousand of them dodging your cursor.

Scheme-to-Scheme, no codec

When the backend is also Scheme (Igropyr), requests and replies are s-expressions — (rpc "/rpc" '(add 1 2 1/2)) comes back (ok 7/2), the exact ratio intact. (web fetch) makes it direct-style over Wasm JSPI; (web ws) / (web sse) push datum streams; (web json) handles everyone else.

Libraries

R6RS-style (library ...) files with (import (math utils)) resolution, dependencies first; exports are advisory because unused code is pruned anyway.

Quick start

$ git clone https://github.com/guenchi/Goeteia
$ cd Goeteia
$ ./run-tests.sh                # every test, both compiler stages
$ ./build-self.sh               # rebuild the compiler with itself

$ echo '(define (fact n) (if (zero? n) 1 (* n (fact (- n 1)))))
(fact 20)' > fact.ss
$ node rt/compile.mjs goeteia.wasm fact.ss fact.wasm
$ node rt/run.mjs fact.wasm
2432902008176640000

Compiled modules run on any engine with Wasm GC and tail calls: Node 22+, current Chrome / Firefox / Safari, wasmtime. Bootstrapping from source needs Chez Scheme; the checked-in compiler wasm works without it.