Compare commits

...

3 commits

Author SHA1 Message Date
af1dd02257 web repl
Some checks failed
webrepl / test (push) Failing after 36s
2025-01-06 00:18:13 -05:00
781e8ba3a3 fixes 2025-01-06 00:15:50 -05:00
622893ebf0 update rustyline 2025-01-02 22:39:29 -05:00
27 changed files with 516 additions and 50 deletions

View file

@ -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!

126
Cargo.lock generated
View file

@ -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"
@ -41,12 +47,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
@ -106,7 +106,7 @@ version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
dependencies = [
"nix 0.29.0",
"nix",
"windows-sys 0.59.0",
]
@ -150,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]]
@ -169,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"
@ -208,18 +220,6 @@ dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases 0.1.1",
"libc",
]
[[package]]
name = "nix"
version = "0.29.0"
@ -228,7 +228,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases 0.2.1",
"cfg_aliases",
"libc",
]
@ -306,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"
@ -417,9 +423,9 @@ dependencies = [
[[package]]
name = "rustyline"
version = "14.0.0"
version = "15.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63"
checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f"
dependencies = [
"bitflags",
"cfg-if",
@ -429,12 +435,12 @@ dependencies = [
"libc",
"log",
"memchr",
"nix 0.28.0",
"nix",
"radix_trie",
"unicode-segmentation",
"unicode-width",
"utf8parse",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -445,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",
@ -495,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"
@ -509,9 +525,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "utf8parse"
@ -525,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"

View file

@ -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"

View file

@ -4,6 +4,7 @@
- [Installation](./install/installation.md)
- [Tour of the REPL](./install/repl.md)
- [The Web REPL](./install/webrepl.md)
# The language

View file

@ -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.

View file

@ -14,7 +14,7 @@ path = "src/main.rs"
[dependencies]
talc-lang = { path = "../talc-lang" }
talc-std = { path = "../talc-std" }
rustyline = "14.0"
rustyline = "15.0"
clap = { version = "4.5", features = ["std", "help", "usage", "derive", "error-context"], default-features = false }
ctrlc = "3.4"
lazy_static = "1.5"

View file

@ -2,7 +2,7 @@ use std::{borrow::Cow, cell::RefCell, rc::Rc};
use rustyline::{
completion::Completer,
highlight::Highlighter,
highlight::{CmdKind, Highlighter},
hint::Hinter,
validate::{ValidationContext, ValidationResult, Validator},
Helper, Result,
@ -105,8 +105,11 @@ impl Highlighter for TalcHelper {
Cow::Owned(format!("\x1b[37m{hint}\x1b[0m"))
}
fn highlight_char(&self, line: &str, _: usize, forced: bool) -> bool {
forced || !line.is_empty()
fn highlight_char(&self, line: &str, _: usize, forced: CmdKind) -> bool {
match forced {
CmdKind::ForcedRefresh => true,
_ => !line.is_empty(),
}
}
}

View file

@ -8,6 +8,6 @@ rust-version = "1.81.0"
workspace = true
[dependencies]
num = { version = "0.4", features = [] }
num = "0.4"
lazy_static = "1.5"
unicode-ident = "1.0"

View file

@ -1,5 +1,5 @@
use core::fmt;
use std::collections::HashMap;
use std::fmt;
use std::rc::Rc;
use crate::chunk::{Arg24, Catch, Chunk, Instruction as I};

View file

@ -3,7 +3,7 @@ use crate::{
symbol::{Symbol, SYM_MSG, SYM_TRACE, SYM_TYPE},
value::Value,
};
use std::{fmt::Display, rc::Rc};
use std::{error::Error, fmt::Display, rc::Rc};
pub type Result<T> = std::result::Result<T, Exception>;
@ -114,6 +114,8 @@ impl Display for Exception {
}
}
impl Error for Exception {}
#[macro_export]
macro_rules! exception {
($exc_ty:expr, $($t:tt)*) => {

View file

@ -1,4 +1,3 @@
use core::{f64, panic};
use std::{borrow::Cow, cmp::Ordering, fmt, hash::Hash, mem::ManuallyDrop, ops, rc::Rc};
use num::{

View file

@ -1,5 +1,3 @@
use core::panic;
use crate::{
parser::ast::{CatchBlock, Expr, ExprKind, LValue, LValueKind, ListItem, TableItem},
value::Value,

View file

@ -1,4 +1,4 @@
use core::fmt;
use std::fmt;
use crate::{
ops::{BinaryOp, UnaryOp},

View file

@ -1,5 +1,4 @@
use core::fmt;
use std::num::ParseFloatError;
use std::{fmt, num::ParseFloatError};
use num::{bigint::ParseBigIntError, Num};

View file

@ -1,4 +1,4 @@
use core::fmt;
use std::fmt;
use std::io::{self, Write};
use num::{bigint::Sign, BigInt};

View file

@ -16,6 +16,7 @@ regex = { version = "1.11", optional = true }
rand = { version = "0.8", optional = true }
[features]
default = ["rand", "regex"]
default = ["rand", "regex", "file"]
rand = ["dep:rand", "dep:num-bigint"]
regex = ["dep:regex"]
file = []

View file

@ -678,6 +678,7 @@ fn spawn_inner(fname: &str, proc: &mut Command, opts: Value) -> Result<Value> {
Ok(c) => c,
Err(e) => throw!(*SYM_IO_ERROR, "{e}"),
};
#[expect(clippy::mutable_key_type)]
let mut table = HashMap::new();
if let Some(stdin) = child.stdin.take() {
let bf = match BufFile::from_raw_fd(stdin.into_raw_fd(), true) {

View file

@ -1,6 +1,5 @@
use std::{
io::{BufRead, Write},
os::unix::ffi::OsStrExt,
sync::Mutex,
time::{SystemTime, UNIX_EPOCH},
};
@ -127,7 +126,7 @@ pub fn env(_: &mut Vm, args: Vec<Value>) -> Result<Value> {
}
let val = std::env::var_os(key.to_os_str());
match val {
Some(val) => Ok(LString::from(val.as_bytes()).into()),
Some(val) => Ok(LString::from(val.as_encoded_bytes()).into()),
None => Ok(Value::Nil),
}
}

View file

@ -619,6 +619,7 @@ pub fn list(vm: &mut Vm, args: Vec<Value>) -> Result<Value> {
pub fn table(vm: &mut Vm, args: Vec<Value>) -> Result<Value> {
let [_, iter] = unpack_args!(args);
let iter = iter.to_iter_function()?;
#[expect(clippy::mutable_key_type)]
let mut result = HashMap::new();
while let Some(value) = vmcalliter!(vm; iter.clone())? {
let Value::List(l) = value else {
@ -851,6 +852,7 @@ pub fn count(vm: &mut Vm, args: Vec<Value>) -> Result<Value> {
let [_, iter] = unpack_args!(args);
let iter = iter.to_iter_function()?;
#[expect(clippy::mutable_key_type)]
let mut map = HashMap::new();
while let Some(v) = vmcalliter!(vm; iter.clone())? {

View file

@ -1,5 +1,3 @@
#![allow(clippy::mutable_key_type)]
use talc_lang::{
symbol::{symbol, Symbol},
vm::Vm,
@ -7,7 +5,6 @@ use talc_lang::{
pub mod collection;
pub mod exception;
pub mod file;
pub mod format;
pub mod ints;
pub mod io;
@ -16,6 +13,8 @@ pub mod math;
pub mod string;
pub mod value;
#[cfg(feature = "file")]
pub mod file;
#[cfg(feature = "rand")]
pub mod random;
#[cfg(feature = "regex")]
@ -24,7 +23,6 @@ pub mod regex;
pub fn load_all(vm: &mut Vm) {
collection::load(vm);
exception::load(vm);
file::load(vm);
format::load(vm);
io::load(vm);
iter::load(vm);
@ -33,6 +31,8 @@ pub fn load_all(vm: &mut Vm) {
string::load(vm);
value::load(vm);
#[cfg(feature = "file")]
file::load(vm);
#[cfg(feature = "rand")]
random::load(vm);
#[cfg(feature = "regex")]

View file

@ -130,11 +130,14 @@ pub fn copy_inner(value: Value) -> Result<Value> {
Ok(v?.into())
}
Value::Table(t) => {
#[expect(clippy::mutable_key_type)]
let t = Rc::unwrap_or_clone(t).take();
let v: Result<HashMap<HashValue, Value>> = t
.into_iter()
.map(|(k, v)| copy_inner(v).map(|v| (k, v)))
.collect();
Ok(v?.into())
}
Value::Native(ref n) => match n.copy_value()? {

16
talc-web/Cargo.toml Normal file
View file

@ -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"

16
talc-web/README.md Normal file
View file

@ -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.

29
talc-web/index.html Normal file
View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Talc Web REPL</title>
<script type="module" src="main.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="terminal">
<div id="history">
<div>Talc Web REPL</div>
<div>For help, see
<a href="https://talc.trimill.xyz/" target="_blank">the docs</a>
or
<a href="https://g.trimill.xyz/trimill/talc" target="_blank">the repository</a>
</div>
</div>
<div class="input_line">
<div id="prompt" class="prompt">&gt;&gt;&nbsp;</div>
<div id="input" style="display: inline-block" contenteditable="true"></div>
</div>
</div>
</body>
</html>

161
talc-web/main.js Normal file
View file

@ -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();
}

51
talc-web/src/lib.rs Normal file
View file

@ -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<dyn std::error::Error>;
struct Talc {
vm: Vm,
globals: Vec<Symbol>,
}
impl Talc {
pub fn eval_line(&mut self, line: &str) -> Result<Option<String>, 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<Talc> = {
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<Option<String>, String> {
TALC.with_borrow_mut(|t| t.eval_line(line))
.map_err(|e| e.to_string())
}

57
talc-web/style.css Normal file
View file

@ -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;
}