8. Wasm, the playground, watch, and a language server#

The whole compiler also builds to wasm, about 4.4 MB (1.4 MB gzipped), and runs in a browser with no server. The playground uses it: type Temper on the left, and see errors, the program's output, its tests' results, and all seven backends' code on the right, updated as you type. The editor also underlines errors, shows types on hover, goes to definitions (into std's and core's sources too), completes names and members, finds references, renames, and shows a call's signature as you type its arguments.

source, after 200 msdiagnostics, firstthe resthover, definition,completions, references,rename, signature helpRunEditor page: main.tsCompiler worker:compiler-worker.tstemper-wasm: analyzefinish: run and backendsJS worker: runs thegenerated JavaScript
source, after 200 msdiagnostics, firstthe resthover, definition,completions, references,rename, signature helpRunEditor page: main.tsCompiler worker:compiler-worker.tstemper-wasm: analyzefinish: run and backendsJS worker: runs thegenerated JavaScript
Diagram source
flowchart LR
  editor["Editor page: main.ts"] -->|"source, after 200 ms"| worker["Compiler worker: compiler-worker.ts"]
  worker --> analyze["temper-wasm: analyze"]
  analyze -->|"diagnostics, first"| editor
  analyze --> finish["finish: run and backends"]
  finish -->|"the rest"| editor
  editor -->|"hover, definition, completions, references, rename, signature help"| worker
  editor -->|"Run"| jsworker["JS worker: runs the generated JavaScript"]

One compile, everything at once#

The page settles the source once, then reuses that one settled library for everything. compile does it all in one call; the page calls it in two halves, analyze and then finish, so errors show before the slower run and backends:

  1. its diagnostics, with positions counted as JavaScript and editors count them;
  2. if asked, a run on the interpreter, with its tests;
  3. tmpl, translated by each backend the page asks for, through the same compile path as tempest build.

std is settled the first time it's needed and kept, since it never changes and settling it takes most of a translation's time.

The answer is one JSON object, shaped like this:

{"diagnostics": [],
 "run": {"output": ["triple(14) = 42"], "failure": null},
 "backends": {"py": {"files": [{"path": "main/…", "text": "…"}]},
              "rust": {"files": ["…"]}}}

analyze gives just {"diagnostics": [...]}, and finish the rest. Every export, and why it's a plain C ABI with JSON rather than wasm-bindgen, is in the wasm API, in detail and its decisions.

A REPL session lives there too, for the page's REPL tab: repl_start, repl_input, repl_complete, and repl_reset settle and run one input at a time against what earlier ones declared, with tempest repl's transcript text.

Run shows the interpreter's results beside the generated JavaScript's, run in a worker of its own. So the playground is also a quick check that two very different ways of running a program agree.

The page#

The messages between the page and its workers, the limits on runaway code, CodeMirror's part, and permalinks are in the page's messages, limits, CodeMirror, and permalinks, in detail.

Editor queries#

analyze keeps the settled library in the wasm, inside an Analysis, until the next source. The editor asks it questions at a position, and the answers come from what settling already worked out, with nothing settled again:

"No member" errors span obj.name, as Kotlin Temper's do; the editor underlines just the name.

A query names the node at the position from each name's symbol (Scopes), each member read's resolution, and each declaration's and expression's type (Typing).

Analysis counts bytes in a file. Editors count UTF-16 code units, so the wasm converts once, at its boundary, with Position and byte_offset. A query waits until the worker has settled the text on screen, so positions always match. How a query finds what's at a position, and what it costs: temper-analysis, in detail.

watch and a language server#

The language server, tempest lsp, exists (page 9); watch doesn't yet. Both reuse what's here rather than add a second compiler:

DiskSources: layersUnsaved editor buffersSettle the libraryAnalysis: diagnostics,hover, definition,completionsLanguage serverPlaygroundCompile path and testrunswatch: rebuild andretest
DiskSources: layersUnsaved editor buffersSettle the libraryAnalysis: diagnostics,hover, definition,completionsLanguage serverPlaygroundCompile path and testrunswatch: rebuild andretest
Diagram source
flowchart LR
  disk["Disk"] --> sources["Sources: layers"]
  buffers["Unsaved editor buffers"] --> sources
  sources --> settle["Settle the library"]
  settle --> analysis["Analysis: diagnostics, hover, definition, completions"]
  analysis --> lsp["Language server"]
  analysis --> playground["Playground"]
  settle --> compile["Compile path and test runs"]
  compile --> watch["watch: rebuild and retest"]

How watch might look, and what it needs first: watch, in detail.

Where in the code#

More: site/playground/README.md, and notes/design.md, section 11, on watch and the language server.

In detail: Wasm and the playground in detail: each decision with its alternatives, the wasm API and the page's messages, sizes, the tests, and known weaknesses.