From af1dd02257e371ee08885d672619acc773e00985 Mon Sep 17 00:00:00 2001 From: trimill Date: Mon, 6 Jan 2025 00:18:13 -0500 Subject: [PATCH] web repl --- .forgejo/workflows/webrepl.yaml | 40 ++++++++ Cargo.lock | 92 +++++++++++++++++- Cargo.toml | 2 +- docs/src/SUMMARY.md | 1 + docs/src/install/webrepl.md | 18 ++++ talc-web/Cargo.toml | 16 ++++ talc-web/README.md | 16 ++++ talc-web/index.html | 29 ++++++ talc-web/main.js | 161 ++++++++++++++++++++++++++++++++ talc-web/src/lib.rs | 51 ++++++++++ talc-web/style.css | 57 +++++++++++ 11 files changed, 480 insertions(+), 3 deletions(-) create mode 100644 .forgejo/workflows/webrepl.yaml create mode 100644 docs/src/install/webrepl.md create mode 100644 talc-web/Cargo.toml create mode 100644 talc-web/README.md create mode 100644 talc-web/index.html create mode 100644 talc-web/main.js create mode 100644 talc-web/src/lib.rs create mode 100644 talc-web/style.css diff --git a/.forgejo/workflows/webrepl.yaml b/.forgejo/workflows/webrepl.yaml new file mode 100644 index 0000000..f842352 --- /dev/null +++ b/.forgejo/workflows/webrepl.yaml @@ -0,0 +1,40 @@ +name: webrepl +on: [push] +#on: +# push: +# branches: +# - main +jobs: + test: + runs-on: docker + + container: + image: rust:1.83-alpine + + steps: + - name: Install dependencies + run: apk add --no-cache git nodejs util-linux + + - name: Install wasm-pack + run: cargo install wasm-pack + + - name: Checkout + uses: actions/checkout@v4 + + - name: Build talc-web + run: wasm-pack build --release --no-typescript --no-pack --target web + working-directory: talc-web + + - name: Save artifacts + uses: forgejo/upload-artifact@v4 + with: + name: webrepl + path: | + *.html + *.js + *.css + pkg/ + if-no-files-found: error + + - name: Done + run: echo done! diff --git a/Cargo.lock b/Cargo.lock index c764966..f5aa90d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -144,8 +150,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -163,6 +171,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -288,6 +306,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -427,9 +451,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "syn" -version = "2.0.94" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", @@ -477,6 +501,16 @@ dependencies = [ "talc-macros", ] +[[package]] +name = "talc-web" +version = "0.1.0" +dependencies = [ + "getrandom", + "talc-lang", + "talc-std", + "wasm-bindgen", +] + [[package]] name = "unicode-ident" version = "1.0.14" @@ -507,6 +541,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index ecfe00c..60d5ad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["talc-lang", "talc-bin", "talc-std", "talc-macros"] +members = ["talc-lang", "talc-bin", "talc-std", "talc-macros", "talc-web"] resolver = "2" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 8bf81f2..1d7ef76 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -4,6 +4,7 @@ - [Installation](./install/installation.md) - [Tour of the REPL](./install/repl.md) +- [The Web REPL](./install/webrepl.md) # The language diff --git a/docs/src/install/webrepl.md b/docs/src/install/webrepl.md new file mode 100644 index 0000000..3029d26 --- /dev/null +++ b/docs/src/install/webrepl.md @@ -0,0 +1,18 @@ +# The Web REPL + +As an alterative to the command-line REPL, you can try out Talc in the browser +at (https://talc.trimill.xyz/repl)[https://talc.trimill.xyz/repl]. Although +the full Talc language and standard library (aside from file I/O and process +control) are available, editing features are more limited than the command-line +interface - especially of note is the lack of syntax highlighting and tab +completion. + +## Usage + +Just like the command-line REPL, enter an expression and press Enter to +evaluate it. Use the up and down arrows to access input history. + +For multiline editing, use Shift+Enter to insert a newline. Hold the Control +key while pressing the up or down arrows to move the cursor between lines. + +To clear the screen, press Ctrl+L. diff --git a/talc-web/Cargo.toml b/talc-web/Cargo.toml new file mode 100644 index 0000000..32f0e98 --- /dev/null +++ b/talc-web/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "talc-web" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[lints] +workspace = true + +[dependencies] +talc-lang = { path = "../talc-lang" } +talc-std = { path = "../talc-std", default-features = false, features = ["rand", "regex"] } +getrandom = { version = "0.2", features = ["js"] } +wasm-bindgen = "0.2" diff --git a/talc-web/README.md b/talc-web/README.md new file mode 100644 index 0000000..47bd9dc --- /dev/null +++ b/talc-web/README.md @@ -0,0 +1,16 @@ +# Talc Web + +The `talc-web` crate provides a WASM interface to execute Talc expressions +in a REPL. Together with the associated HTML, CSS, and JavaScript files, this +creates an alternative to the command-line REPL. + +## Building + +In the `talc-web` directory, run: + +```bash +wasm-pack build --release --no-typescript --no-pack --target web +``` + +See (the main README.md)[../README.md] for more information about building +Talc. diff --git a/talc-web/index.html b/talc-web/index.html new file mode 100644 index 0000000..3752624 --- /dev/null +++ b/talc-web/index.html @@ -0,0 +1,29 @@ + + + + + + + Talc Web REPL + + + + + + +
+
+
Talc Web REPL
+
For help, see + the docs + or + the repository +
+
+
+
>> 
+
+
+
+ + diff --git a/talc-web/main.js b/talc-web/main.js new file mode 100644 index 0000000..a592973 --- /dev/null +++ b/talc-web/main.js @@ -0,0 +1,161 @@ +"use strict"; + +import init, * as talc from "./pkg/talc_web.js"; +await init(); + +/* + * DOM + */ + +const $ = (q) => document.querySelector(q); + +function newEl(tag, attrs, children) { + const el = document.createElement(tag); + for (const attr in attrs) { + el.setAttribute(attr, attrs[attr]); + } + for (const child of children) { + if (typeof(child) === "string") { + el.appendChild(document.createTextNode(child)); + } else { + el.appendChild(child); + } + } + return el; +} + +/* + * History + */ + +let historyEntries = [""]; +let historyIndex = 0; +let currentLine = ""; + +function prevHistory() { + const inputEl = $("#input"); + if (historyIndex === 0) { + currentLine = inputEl.innerText; + } + historyIndex = Math.min(historyIndex+1, historyEntries.length); + const text = historyEntries[historyEntries.length - historyIndex]; + inputEl.innerText = text; + + document.getSelection().setPosition(inputEl, inputEl.childNodes.length); +} + +function nextHistory() { + const inputEl = $("#input"); + historyIndex = Math.max(historyIndex-1, 0); + let text; + if (historyIndex === 0) { + text = currentLine; + } else { + text = historyEntries[historyEntries.length - historyIndex]; + } + inputEl.innerText = text; + + document.getSelection().setPosition(inputEl, inputEl.childNodes.length); +} + +/* + * REPL + */ + +function nextLine() { + const newPrompt = newEl("div", {"class": "prompt"}, []); + newPrompt.innerHTML = $("#prompt").innerHTML; + + const newInput = newEl("div", {}, []); + newInput.innerHTML = $("#input").innerHTML; + + $("#history").appendChild(newEl( + "div", + {"class": "input_line"}, + [newPrompt, newInput] + )); + + $("#input").innerHTML = ""; + + currentLine = ""; + historyIndex = 0; + +} + +function execLine() { + const inputLine = $("#input").innerText; + nextLine(); + historyEntries.push(inputLine); + + try { + const fixedInput = inputLine.replaceAll("\u00a0", " "); + const out = talc.eval_line(fixedInput); + if (out !== undefined) { + $("#history").appendChild(newEl("div", {}, [out])); + } + } catch (e) { + $("#history").appendChild(newEl("div", {}, [ + newEl("span", {"class": "error"}, ["Error: "]), + newEl("pre", {"class": "err_msg"}, [e.toString()]) + ])); + } +} + +/* + * Input + */ + +function handleCtrlKey(event) { + switch (event.code) { + case "KeyL": + event.preventDefault(); + $("#history").innerHTML = ""; + break; + case "KeyC": + event.preventDefault(); + nextLine(); + break; + } +} + +function keyPressed(event) { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + execLine() + } else if (event.ctrlKey) { + handleCtrlKey(event); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + prevHistory(); + } else if (event.key === "ArrowDown") { + event.preventDefault(); + nextHistory(); + } + + window.scrollTo(0, document.body.scrollHeight); +} + +/* + * Listeners + */ + +$("#input").addEventListener("keydown", keyPressed); +$("#input").focus(); + +$("#terminal").addEventListener("click", (event) => { + if (event.target.id === "terminal" || event.target.id === "input") { + $("#input").focus(); + } +}) + +/* + * Query parameter + */ + +const urlParams = new URLSearchParams(window.location.search); +const initialLine = urlParams.get("eval"); + +if (initialLine !== null) { + $("#input").innerText = initialLine; + execLine(); +} diff --git a/talc-web/src/lib.rs b/talc-web/src/lib.rs new file mode 100644 index 0000000..a290ae5 --- /dev/null +++ b/talc-web/src/lib.rs @@ -0,0 +1,51 @@ +use std::{cell::RefCell, rc::Rc}; + +use talc_lang::{ + compiler, parser, + symbol::{symbol, Symbol}, + value::Value, + vm::Vm, +}; +use wasm_bindgen::prelude::wasm_bindgen; + +type BoxError = Box; + +struct Talc { + vm: Vm, + globals: Vec, +} + +impl Talc { + pub fn eval_line(&mut self, line: &str) -> Result, BoxError> { + let expr = parser::parse(line)?; + let func = compiler::compile_repl(&expr, &mut self.globals)?; + let func = Rc::new(func); + let res = self.vm.run_function(func.clone(), vec![func.into()])?; + let res_str = (res != Value::Nil).then(|| format!("{res:#}")); + self.vm.set_global(symbol!("_"), res); + Ok(res_str) + } +} + +thread_local! { + static TALC: RefCell = { + let mut vm = Vm::new(128, Vec::new()); + talc_std::load_all(&mut vm); + + let globals = vec![symbol!("_")]; + vm.set_global(symbol!("_"), Value::Nil); + + RefCell::new(Talc { vm, globals }) + } +} + +#[wasm_bindgen(start)] +pub fn start() { + TALC.with(|_| ()); +} + +#[wasm_bindgen] +pub fn eval_line(line: &str) -> Result, String> { + TALC.with_borrow_mut(|t| t.eval_line(line)) + .map_err(|e| e.to_string()) +} diff --git a/talc-web/style.css b/talc-web/style.css new file mode 100644 index 0000000..3aa73d1 --- /dev/null +++ b/talc-web/style.css @@ -0,0 +1,57 @@ +body { + font-family: monospace; + font-size: 14pt; + + background-color: #14171d; + color: #c7c6c3; + + margin: 20px; +} + +.input_line { + display: flex; +} + +#prompt { + margin-bottom: 100px; +} + +#input { + width: 100%; +} + +#input:focus { + outline: none; +} + +div, pre { + margin: 0; + padding: 0; +} + +#terminal { + min-height: calc(100vh - 60px - 8px); + padding: 10px; + border: 4px solid #4d4754; + border-radius: 6px; +} + +a { + color: #82bfb3; +} + +a:visited { + color: #82bfb3; +} + +pre.err_msg { + display: inline; +} + +.error { + color: #cc5c5c; +} + +.prompt { + color: #789ebf; +}