The playground

The playground at torb.dev/play and in the course runs TorbScript in the browser - torb as WebAssembly runs the program in its VM and its language server completes and checks as you type, with no server - and what a program can do there.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

The playground runs a program in your browser the way torb run runs it on your machine: the toolchain checks it, lowers it and runs it in the VM. It is the same torb, compiled to WebAssembly, so a program prints in the playground what it prints anywhere else, and the editor has the same language server an editor on your machine has: completion, the checker's errors as you type, hover and formatting. Nothing is sent to a server; the page loads the toolchain once and works offline from then on.

Synopsis#

torb.dev/play                 A page with the examples, an editor, Run and the output
torb.dev/play#code=<source>   The same page with a program in it: the address is the link to share
torb.dev/play#example=<id>    The same page with one of the examples
Ctrl+Enter, Cmd+Enter         Run
Ctrl+Space                    Completion; it also opens by itself as you type a name or a `.`
F12, Ctrl or Cmd and a click  Go to the definition of what is under the cursor
F2                            Rename it
Ctrl+.                        The quick fixes of the error under the cursor
Shift+Alt+F                   Format, as `torb format` does
Tab, Shift+Tab                Indent and outdent by two spaces; Escape, then Tab, leaves the editor, and Ctrl+M
                              switches Tab over to moving the focus for good

sh playground/build.sh              Build it: the compiler's C for browser-wasm64, compiled by emscripten
node playground/smoke-test.mjs      Run hello world in the result under node, and the language server
sh playground/editor/build.sh       Build the editor from its source and lock file (--check: is it current?)

What it does#

Where a program runs#

Press Run and the page hands the source to a worker thread, which starts a fresh copy of the toolchain, writes the source as main.trb into a file system in memory, and runs torb run main.trb. What the program prints appears under the editor as it prints it; what the checker reports appears there too, and the place it names is marked in the editor. A place such as main.trb:2:1 in the output moves the cursor there.

While a program runs, Run is Stop: a program that never ends is stopped there, and one that prints more than a megabyte is stopped by the playground. Every run starts from nothing - an empty file system, a fresh memory - so one run never sees what the last one left.

The editor#

The editor is Monaco, the editor of VS Code, connected to torb lsp, which runs in a worker of its own in the page - on /play and in every runnable block of the documentation. What the language server does on your machine it does here:

  • Completion as you type a name, and the members of a value after a ., each with its signature, and its documentation beside the list, formatted as it is in VS Code.
  • The checker's errors while you type: a quarter of a second after the last keystroke the text is checked, and every error is a wavy underline; the pointer on it shows the message, and Ctrl+. its quick fixes.
  • Hover shows the declaration of a name, coloured, and its documentation, and signature help the parameters of the call you are in.
  • Go to definition and rename within the program, and Format - the button on /play, or Shift+Alt+F - is torb format.
  • The colours are the site's, in both of its themes: a grammar's at once, and the checker's as soon as it has seen the text - a parameter in its own colour, and a binding that can change underlined.

The language server is one per page and holds every editor of it, and it is independent of the runs: a run never waits for a check, and a check never waits for a run.

A runnable block starts as the code the site highlighted, in a frame of exactly the editor's size, type and spacing. When the pointer comes over it, or the keyboard's focus into it, the editor takes its place, and nothing on the page moves; the language server starts with your first click into the code. So a page with many runnable blocks loads nothing until you reach for one. /play has the editor at once, and on a wide screen the editor and the output stand side by side. On a phone or a tablet, where Monaco is not made for touch, a block is a plain text field that colours and runs, without the language server.

The examples#

/play opens with an example, and Examples above the editor lists the others by category, each with its title, its level and one line about it; on the German site they are in German. Choosing one replaces the code - after a question, if you changed what was there - and the address names the example, #example=<id>, until you change the code; from then on it carries the code itself.

What a program can do there#

The target of the playground is browser-wasm64: OperatingSystem.current is .Browser and Architecture.current is .Wasm64. What each package that reaches outside of the program does there follows one rule: real, simulated, or an error at the import.

PackageIn the playground
printing, std/timereal: the output appears under the editor, the clock is the machine's
std/fssimulated: a file system in memory, empty when the program starts, gone when it ends
std/osan empty environment; the system questions answer Unsupported, Directories.temporary() is /tmp, and Entropy is real: crypto.getRandomValues of the page
standard inputat its end: readLine() answers Ok(None), and so does the first read of standardInput()
std/tasktasks take turns on one thread, in the order TORB_WORKERS=1 gives everywhere
std/processan error at the import: a program in the browser starts no processes
std/network, std/http, std/tls, std/dnsan error at the import: a program in the browser has no network

Sharing#

On the playground's page, the address changes as you type: behind #code= stands the source, compressed. Copying the address shares the program, and opening it opens the program. The part behind the # is never sent to a server.

Exercises#

In the course, an exercise is a playground with an expected output. Beside Run it has Reset, which brings the starting code back, and Show solution, which puts the solution into the editor; after a run, a line says whether the output is the expected one.

What the browser needs#

WebAssembly with exception handling - Chrome and Edge 95, Firefox 100, Safari 15.2 or newer - and JavaScript. The toolchain is about 13 MB, 4 MB as the server sends it compressed, fetched once when you first click into an editor or press Run - at once on /play - and then kept by the browser. The editor is 3.3 MB more, 0.85 MB compressed, fetched when the pointer first comes over a runnable block, with its stylesheet (20 KB compressed) and the font of its icons (75 KB compressed, when an icon is first shown); a phone or a tablet never fetches it. The page makes no request to another host and stores nothing on the device.

How the language server runs in the page#

A browser cannot wait inside a read: a message reaches the worker only once its thread is back in its event loop. So the worker runs the real torb lsp - the same server, over the same standard input and output, as an editor on your machine starts - and the runtime's browser target makes its standard input a thing to wait for without blocking (runtime/os/browser.c). A read that finds nothing yet waits as an operation of the runtime, and where the server's scheduler would sleep it returns to the worker instead. The worker frames each message of the editor with its Content-Length header into standard input and lets the server go on until it waits again; what the server writes it cuts into messages and hands to the editor. The server asks for its timers the same way, and the worker calls it again when they are due. Longer work - the check of the workspace, a rename - returns to the worker every 50 ms, so a keystroke's completion is answered in the middle of it rather than after it.

How it is built#

The compiler is TorbScript that emits C, so the toolchain of the browser is the compiler's own C for the target browser-wasm64 (torb build ./compiler --emit-c --target browser-wasm64) together with runtime/, compiled by emscripten, pinned by version in playground/build.sh. std/ is embedded into the WebAssembly. torb docs site copies what the script wrote into assets/ of the site, and the site image builds it from the release's own source.

The editor is playground/playground-editor.js, with playground-editor.css and playground-codicon.ttf beside it: Monaco with the playground's own part, playground/editor/ - editor.mjs registers TorbScript and a provider for each thing the language server answers, protocol.mjs speaks JSON-RPC to the language worker and translates its answers into what Monaco shows, grammar.mjs colours the code before the server has checked it (and the code blocks of the documentation it shows), and theme.mjs makes the site's tokens a Monaco theme. The three files are committed, so building the playground needs neither npm nor the network: sh playground/editor/build.sh rebuilds them with npm ci and esbuild at the exact versions of playground/editor/package-lock.json, where the editor changes, and the result is committed with the change; CI checks that the committed files are what their source builds.

playground/examples/ holds the programs the gallery on torb.dev/play offers beside the empty editor, one file each and index.json naming them: id, file, title, a one-sentence description, a category (Basics, Types, Errors, Collections, DSLs, Fun), a level (beginner or intermediate), "default": true on the one example the page opens with, and the same title/description in German under de. playground/build.sh copies index.json and every .trb file it names into build/playground/examples/, beside torb.wasm, so the page serves them the way it serves the toolchain itself - no server call.

Every example is also a program sh tools/gates.sh a runs: each <name>.trb has a <name>.expected beside it, and the gate runs it with torb run (the playground never builds natively) and compares. Adding one is the same shape as a program of tests/language/:

  1. Write playground/examples/<name>.trb - it has to run in the browser (no std/process or network, std/fs is in memory, a program stops well under a second) and print something worth seeing.
  2. Run it and save what it prints: build/release/torb run playground/examples/<name>.trb > playground/examples/<name>.expected.
  3. Add its entry to index.json, English and German.
  4. sh tools/gates.sh a - the check playground/examples and playground/examples against .expected gates catch a mistake in either the program or the manifest.

Examples#

An import the browser does not have is an error at the import. The program below runs on every machine; in the playground the run stops before its first instruction, and the editor underlines the import as soon as it is typed:

use Process from "std/process"

print Process.arguments()
error: `std/process` does not exist in the browser: a program in the browser starts no processes
 --> main.trb:1:1
  |
1 | use Process from "std/process"
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  = The playground runs a program in the browser: printing and the clock are real, `std/fs` is in memory

A file written, read back and removed, in the file system in memory - and on your machine, where the same program runs in the working directory:

use File from "std/fs"

File.writeText("notes.txt", "kept in memory").expect("written")
print File.readText("notes.txt").expect("read")
File.remove("notes.txt").expect("removed")
// prints kept in memory

Building the playground and running its smoke test, from the root of a checkout with a bootstrapped torb:

$ sh playground/build.sh
emitting the compiler's C for browser-wasm64
compiling in emscripten/emsdk:6.0.10
linking build/playground/torb.js and build/playground/torb.wasm
wrote build/playground/: torb.wasm 13510389 bytes, 4093927 gzipped
$ node playground/smoke-test.mjs
ok    hello world (503 ms)
...
ok    the language server answers initialize (229 ms)
ok    a document with an error is published with its diagnostic, as a marker (244 ms): Cannot find `prnt` here
ok    a change while typing is checked again (309 ms)
ok    completion at a member access answers its members as suggestions (111 ms): 69 suggestions
...
ok    shutdown and exit end the server with 0 (605 ms)
ok    the site's highlighting becomes semantic tokens, one per line of a token (0 ms)
  • torb run - the command the playground runs, and the VM it runs in.
  • torb lsp - the language server the editor talks to.
  • Compile-time branches - match OperatingSystem.current, which has a .Browser arm.
  • torb docs site - the site that serves the playground and turns runnable blocks into it.
  • std/os - what the system questions answer in the browser.
  • Verify your work - sh tools/gates.sh a, which runs every example of the gallery.
Esc