Everything beside this is live rendered by Goeteia.
The Scheme below is compiled to WebAssembly in your browser and mounted live.
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.
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.
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…
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.
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.
;; one form: the markup AND its css -- this is the ;; real definition of the cards further down this page (define-component (card title . body) (style (background (var bg2)) (border-radius (px 10)) ("h3" (color (var lapis)))) ; descendants, (div (h3 ,title) (p ,@body))) ; :hover, @media… ;; equal style sets intern to ONE generated class: ;; nine cards below share a single rule (sx (button (@ (on-click ,bump!)) ; live holes: the "clicked " ,(signal-ref n))) ; tree builds ONCE, ; holes become effects
(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.
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))
(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.
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))))))
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.
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.
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.
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.
syntax-rules and procedural syntax-case with fenders, nested ellipses and datum->syntax, running in a compile-time interpreter with hygiene by renaming.
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.
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.
(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.
(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.
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.
R6RS-style (library ...) files with (import (math utils)) resolution, dependencies first; exports are advisory because unused code is pruned anyway.
$ 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
2432902008176640000Compiled 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.