Backend-less SPA for trying Rell in the browser. Live: https://chromiaproject.github.io/rell-playground/
Three modes:
- Run — paste a complete program, click Run. For pure functions,
print, expression evaluation, struct/enum defs, etc. Output appears in the right pane. (Entity / query / operation declarations aren't allowed here — use SQL dry-run for those.) - SQL dry-run — declare entities + a
query main(); the playground compiles every@-expression to SQL and shows the statements postchain would issue in the SQL pane. Nothing actually executes (there's no database), so queries run against an empty result set. - REPL
The Rell compiler + interpreter is compiled ahead-of-time to JavaScript by
TeaVM. The browser loads the resulting ESM module directly
— no JVM bytecode interpreter, no CheerpJ runtime, just a single .js file.
flowchart TD
SPA["SPA — TypeScript, Vite"]
Worker["Web Worker (ESM)"]
Bridge["rell-playground-bridge.js<br/>TeaVM AOT JS"]
API["PlaygroundJsBridge<br/>@JSExport static API<br/>version / runFile / runModule / repl*"]
Repl["ReplSession<br/>wraps ReplInterpreter<br/>(Run + REPL modes)"]
Module["ModuleSession<br/>compiles source as module 'main'<br/>(SQL dry-run)"]
Sql["CapturingSqlManager<br/>records SQL before<br/>returning empty results"]
Channel["BufferedReplChannel<br/>emits a JSON event envelope"]
SPA --> Worker
Worker --> Bridge
Bridge --> API
API --> Repl
API --> Module
Repl --> Sql
Module --> Sql
Repl --> Channel
Module --> Channel
The worker imports rell-playground-bridge.js as ESM and calls the
@JSExport-annotated static methods (version, runFile, runModule,
replCreate, replExecute, replDispose) directly. Each call returns a JSON
envelope {"ok": bool, "events": […]} whose events are stdout lines, REPL
value prints, compiler diagnostics, runtime errors, and sql entries. The
worker forwards each as a postMessage; main.ts routes them — sql → SQL
pane, everything else → the output panel.
CapturingSqlManager replaces Rell's NoConnSqlManager. Every SQL string Rell
hands the executor is appended to the event channel; the executor then returns
an empty result (rather than throwing "no database connection"), so a whole
routine's worth of statements is captured in one run instead of just the first.
This is something the stock rell.sh can't do — without --db-url it bails on
the first SQL call.
./gradlew assembleAll # → web/dist/
# Or, for dev:
./gradlew dev # http://localhost:5173The bridge JS lands at web/public/teavm/rell-playground-bridge.js (mirrored
there by :bridge:copyTeavmToWeb). Vite copies web/public/ verbatim into
web/dist/, so the worker's static /teavm/rell-playground-bridge.js import
resolves both in dev and prod.
.github/workflows/ci.yml runs three jobs:
- bridge — runs
:bridge:build, uploadsweb/public/teavm/as an artifact. - build — downloads the bridge artifact and runs
:web:assembleto produceweb/dist/. Also runs the Vitest survival kit against the bridge. - deploy — on
master, publishesweb/dist/to GitHub Pages.