Goeteia, инструмент веб-разработки на чистом Scheme, который компилируется в WebAssembly в браузере

Пользователь guenchi опубликовал на Hacker News свой проект Goeteia, веб-инструмент на языке Scheme, который наглядно демонстрирует сам себя: код, показанный на странице goeteia.dev, компилируется в WebAssembly прямо в браузере посетителя и тут же запускается. Компилятор целиком доставляется на клиент единым модулем goeteia.wasm весом около 70 КБ в сжатом виде (кешируется после первой загрузки), и каждый повторный запуск («Run») заново компилирует показанный исходный код примерно за 15 миллисекунд. Сервера, который бы что-то компилировал заранее, в этой схеме нет вовсе.

Ключевая особенность, компилятор самоприменим: он написан на том же подмножестве Scheme, которое сам компилирует. Автор пересобирает компилятор им самим и получает байт-в-байт идентичный результат по сравнению с эталонной сборкой, то есть достигается неподвижная точка (fixpoint). Это свойство проверяется в CI при каждом изменении кода, а весь тестовый набор прогоняется через обе стадии: через компилятор, собранный обычным хостовым инструментом, и через самоприменимую версию.

В рантайме используются низкоуровневые возможности современного WebAssembly: короткие целые числа (fixnum) хранятся как необёрнутые ссылки i31ref, пары и записи, как структуры сборщика мусора Wasm GC, а проверка eq? компилируется в единственную инструкцию ref.eq. Никакой «теневой» копии данных в JavaScript не ведётся, от хоста нужны только два потоковых импорта ввода-вывода, и ничего больше. Система макросов включает и syntax-rules, и процедурный syntax-case с охранными условиями (fenders), вложенными многоточиями и datum->syntax; всё это работает в компиляционном интерпретаторе с гигиеной через переименование идентификаторов. Вызовы функций типизированы: у каждой функции есть быстрая точка входа под конкретное число аргументов и универсальная точка входа для замыканий, это удешевляет функции с переменным числом аргументов и apply, а каждый хвостовой вызов компилируется в инструкцию return_call. По заявлению автора, цикл на 100 миллионов итераций выполняется в константном объёме стека примерно за 150 миллисекунд. Продолжения для нелокального выхода (escape continuations) реализованы поверх экспериментального предложения WebAssembly по обработке исключений: захват такого продолжения стоит O(1), обычный путь выполнения, это один блок try, а обработчики входа-выхода (winders, аналог dynamic-wind) разворачиваются от внутренних к внешним.

Для веб-разработки в проекте есть библиотека «web sx», шаблоны поверх точечных реактивных сигналов «web reactive», рендерер «web html» и FFI «web js», напрямую обращающийся к хостовому JavaScript; сама страница goeteia.dev построена именно на этом стеке. Библиотека «web three» строит реактивные сцены Three.js так же, как «web sx» строит DOM, а «web gl» управляет «сырым» WebGL через командный буфер, один вызов моста на кадр, при этом шейдеры пишутся как s-выражения через «web glsl»: например, заголовок на странице нарисован именно так, буквами из точек в стиле матричного принтера, одним вершинным шейдером и одним вызовом отрисовки. Если бэкенд тоже написан на Scheme (проект-компаньон Igropyr), запросы и ответы передаются как s-выражения: например, запрос rpc к маршруту /rpc с телом (add 1 2 1/2) возвращает (ok 7/2), точное рациональное число без потери точности, а не число с плавающей запятой. Библиотека «web fetch» выполняет такие запросы в прямом стиле поверх экспериментального предложения WebAssembly JSPI (интеграция с промисами JavaScript), «web ws» и «web sse» передают потоки данных через WebSocket и Server-Sent Events, а «web json» покрывает обычные JSON-API. Модули организованы в стиле R6RS, как файлы library, где раздел import перечисляет пути зависимостей (например, math utils), подключаемых в порядке «сначала зависимости»; экспортируемые имена необязательны, поскольку неиспользуемый код всё равно удаляется при сборке.

Проект открыт на GitHub (github.com/guenchi/Goeteia), и его можно проверить локально: git clone, затем run-tests.sh (весь тестовый набор на обеих стадиях компилятора) и build-self.sh (пересборка компилятора им самим). В качестве примера автор показывает компиляцию функции вычисления факториала: команды node rt/compile.mjs goeteia.wasm fact.ss fact.wasm и node rt/run.mjs fact.wasm возвращают точное значение 20 факториал, 2432902008176640000. Скомпилированные модули работают на любом движке с поддержкой Wasm GC и хвостовых вызовов: Node.js 22+, актуальные версии Chrome, Firefox и Safari, а также wasmtime. Для пересборки компилятора из исходного кода нужен Chez Scheme, но уже готовый .wasm-файл компилятора, включённый в репозиторий, работает и без него. На момент публикации пост на Hacker News набрал 93 балла и 22 комментария при возрасте около 8 часов.

Ключевые факты

  • Компилятор Scheme в WebAssembly целиком доставляется в браузер: модуль goeteia.wasm весит около 70 КБ в сжатом виде, кешируется после первой загрузки, а компиляция показанного на странице кода занимает около 15 миллисекунд на каждый запуск
  • Компилятор самоприменим: написан на том же подмножестве Scheme, которое сам компилирует; пересобранная им самим версия байт-в-байт совпадает с эталоном, это свойство неподвижной точки (fixpoint) проверяется в CI при каждом изменении, а тесты прогоняются через обе стадии компилятора
  • Рантайм экономит на абстракциях: короткие числа, необёрнутые ссылки i31ref, пары и записи, структуры сборщика мусора Wasm GC, проверка eq?, одна инструкция ref.eq; хосту (JavaScript) нужны только два потоковых импорта ввода-вывода, никакой теневой копии данных в JS не хранится
  • Хвостовые вызовы компилируются в инструкцию return_call: цикл на 100 миллионов итераций выполняется в константном стеке примерно за 150 миллисекунд; нелокальные выходы (escape continuations) захватываются за O(1) поверх экспериментального предложения WebAssembly по обработке исключений
  • Для веба есть библиотеки «web sx», «web reactive», «web three» и «web gl», DOM, реактивные сигналы, сцены Three.js и WebGL из s-выражений, а RPC к бэкенду на том же языке (проект-компаньон Igropyr) возвращает точные рациональные числа вроде 7/2, а не числа с плавающей запятой

Почему это важно

Проект показывает, что для WebAssembly уже можно построить полноценный компилятор целого языка, со сборкой мусора, гигиеническими макросами и хвостовыми вызовами, который выполняется прямо в браузере пользователя, без бэкенда и без отдельного сборочного сервера. Компилятор Scheme весит около 70 КБ в сжатом виде, компилирует показанный на странице код примерно за 15 миллисекунд и при этом самоприменим: пересобирает сам себя, и результат байт-в-байт совпадает с эталоном (неподвижная точка, fixpoint), это проверяется в CI при каждом изменении. Автор опирается на новые возможности WebAssembly: типизированные ссылки на функции и GC-структуры для пар и записей, инструкцию return_call для хвостовых вызовов, а также экспериментальные предложения по обработке исключений (для нелокальных выходов) и JSPI, интеграцию с промисами JavaScript для асинхронных вызовов. Это пример того, что современный WebAssembly вырос из узкого числового движка в платформу, способную нести весь стек языка со сложной семантикой, гигиенические макросы, точные рациональные числа, продолжения для выхода, а не только скомпилированный код на C или Rust.

Кому это важно

Разработчикам языковых рантаймов и компиляторов, которые оценивают Wasm GC, хвостовые вызовы и обработку исключений как целевую платформу; веб-разработчикам, которым интересны альтернативы связке JS и TypeScript для клиентского кода, шаблоны «web sx», точечные реактивные сигналы, рендер Three.js и WebGL из s-выражений; сообществу Scheme и Lisp, это редкий пример современного самоприменимого компилятора Scheme, нацеленного именно на браузер, а не на серверный рантайм. Интересно и тем, кто проектирует full-stack-фреймворки: показан вариант RPC поверх s-выражений между Scheme-фронтендом и Scheme-бэкендом (проект-компаньон Igropyr) с сохранением точных рациональных чисел вместо чисел с плавающей запятой.

Как это применить

Проект открыт на GitHub (github.com/guenchi/Goeteia). Локально: git clone, затем run-tests.sh прогоняет весь набор тестов на обеих стадиях компилятора (собранном хостовым инструментом и самоприменимом), а build-self.sh пересобирает компилятор им самим. Пример из документации: функция вычисления факториала сохраняется в файл с расширением .ss, компилируется командой node rt/compile.mjs goeteia.wasm fact.ss fact.wasm и запускается через node rt/run.mjs fact.wasm, на выходе точное значение 20 факториал (2432902008176640000). Скомпилированные модули работают на любом движке с поддержкой Wasm GC и хвостовых вызовов: Node.js 22+, актуальные версии Chrome, Firefox и Safari, а также wasmtime. Для пересборки компилятора из исходного кода нужен Chez Scheme, но уже готовый .wasm-файл компилятора, включённый в репозиторий, работает и без него.

Можно ли доверять

Автор поста на Hacker News, пользователь guenchi, тот же ник указан как владелец GitHub-репозитория проекта, то есть, судя по всему, автор представляет собственную разработку, а не независимый обзор. Из проверяемого: код открыт, в репозитории есть набор тестов (run-tests.sh), а заявление о самоприменимости компилятора можно перепроверить самостоятельно, сборка «компилятор собирает сам себя» и байт-идентичность результата, по описанию автора, проверяются в CI при каждом изменении. На момент сбора материала обсуждение на Hacker News набрало 93 балла и 22 комментария при возрасте поста около 8 часов, заметный, но не экстремальный отклик. Независимой сторонней оценки производительности или примеров использования в реальных проектах в тексте нет: все цифры (70 КБ, 15 миллисекунд, 150 миллисекунд), это утверждения самого автора, не подтверждённые внешними измерениями.

Риски и подводные камни

Проект молодой и нишевый: Scheme как язык для веб-фронтенда, редкость, а экосистема (библиотеки, документация, сообщество) неизбежно на порядки меньше, чем у связки JS, TypeScript и React. Часть возможностей опирается на экспериментальные предложения WebAssembly, обработку исключений (для нелокальных выходов) и JSPI для асинхронных вызовов, эти стандарты ещё не финализированы во всех движках, поэтому список поддерживаемых окружений ограничен: нужны Node.js 22+ и актуальные версии браузеров с поддержкой Wasm GC и хвостовых вызовов. Пересборка компилятора из исходного кода требует Chez Scheme, дополнительная и не самая распространённая зависимость. В тексте нет данных об использовании в реальных проектах, нагрузочных тестах или сравнении производительности с альтернативными способами компиляции в WebAssembly, поэтому заявленные цифры стоит воспринимать как лабораторные показатели автора, а не как гарантию для стороннего проекта.

«Это компилирует не сервер, страница несёт в себе весь компилятор (goeteia.wasm, около 70 КБ в сжатом виде, кешируется после первой загрузки), и каждый запуск заново компилирует исходный код на странице примерно за 15 миллисекунд.»

— сайт проекта Goeteia (goeteia.dev)