Merge branch 'repl'

master
Ondřej Hruška 2 weeks ago
commit 8160815e3f
  1. 132
      Cargo.lock
  2. 1
      Cargo.toml
  3. 26
      README.md
  4. 20
      src/main.rs
  5. 145
      src/repl.rs

132
Cargo.lock generated

@ -16,6 +16,7 @@ dependencies = [
"reqwest", "reqwest",
"rusqlite", "rusqlite",
"rustls", "rustls",
"rustyline",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
@ -185,6 +186,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.45" version = "0.4.45"
@ -238,6 +245,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "clipboard-win"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.5" version = "1.0.5"
@ -380,6 +396,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]] [[package]]
name = "env_filter" name = "env_filter"
version = "2.0.0" version = "2.0.0"
@ -430,6 +452,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "error-code"
version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
[[package]] [[package]]
name = "fallible-iterator" name = "fallible-iterator"
version = "0.3.0" version = "0.3.0"
@ -442,6 +470,17 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fd-lock"
version = "4.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@ -627,6 +666,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.4.2" version = "1.4.2"
@ -984,6 +1032,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.2" version = "0.8.2"
@ -1092,6 +1146,27 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "nibble_vec"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.13.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1242,6 +1317,16 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "radix_trie"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
dependencies = [
"endian-type",
"nibble_vec",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.6" version = "0.8.6"
@ -1390,6 +1475,19 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.13.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.40" version = "0.23.40"
@ -1469,6 +1567,28 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rustyline"
version = "14.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63"
dependencies = [
"bitflags 2.13.0",
"cfg-if",
"clipboard-win",
"fd-lock",
"home",
"libc",
"log",
"memchr",
"nix",
"radix_trie",
"unicode-segmentation",
"unicode-width",
"utf8parse",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -1879,6 +1999,18 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"

@ -19,6 +19,7 @@ mlua = { version = "0.11.6", features = ["lua55", "vendored", "serde", "async",
reqwest = { version = "0.13.4", default-features = false, features = ["rustls-no-provider", "charset", "http2", "system-proxy"] } reqwest = { version = "0.13.4", default-features = false, features = ["rustls-no-provider", "charset", "http2", "system-proxy"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rusqlite = { version = "0.32", features = ["bundled"] } rusqlite = { version = "0.32", features = ["bundled"] }
rustyline = "14"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150" serde_json = "1.0.150"
thiserror = "2.0.18" thiserror = "2.0.18"

@ -3,10 +3,32 @@
`a` embeds a Lua interpreter and adds a standard library and IO access, so a script can reach the network, a database, the filesystem, and the terminal without additional setup. `a` embeds a Lua interpreter and adds a standard library and IO access, so a script can reach the network, a database, the filesystem, and the terminal without additional setup.
```sh ```sh
a file.lua a file.lua # run a script
a # start an interactive REPL
``` ```
The interface is a single command: `a` runs the script at the given path. Run `a` with a script path to execute it, or with no arguments to start an
interactive REPL.
## The REPL
Running `a` with no arguments starts a read-eval-print loop in the same
sandboxed environment used for scripts, with the full standard library
available. It is convenient for trying out the library or exploring data.
- Bare expressions print their value (`1 + 1` → `2`); tables are pretty-printed
with `utils.dump`.
- Statements that span several lines (a `for` loop, a function body) are
detected as incomplete and keep reading until they parse.
- Async calls such as `os.sleep`, `http.get`, and `task.join` suspend and resume
just as they do in a script — each entry runs on the async executor.
- Line editing and history are provided, with history persisted to
`~/.a_history`. Press Ctrl-D to exit (`os.exit` is not available in the
sandbox).
Each entry is compiled as its own chunk, so a `local` declared on one line is
not visible on the next — assign to a global (drop the `local`) to keep a value
across entries.
## What it is ## What it is

@ -2,15 +2,17 @@ use std::path::PathBuf;
use clap::Parser; use clap::Parser;
mod repl;
mod sandbox; mod sandbox;
mod stdlib; mod stdlib;
/// Runs a Lua script in a customized environment /// Runs a Lua script in a customized environment, or an interactive REPL when
/// no script path is given.
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about)] #[command(version, about)]
struct Args { struct Args {
/// Path to the Lua script to execute /// Path to the Lua script to execute. Omit to start an interactive REPL.
filepath: PathBuf, filepath: Option<PathBuf>,
} }
#[tokio::main] #[tokio::main]
@ -19,7 +21,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse(); let args = Args::parse();
let source = tokio::fs::read_to_string(&args.filepath).await?; let lua = sandbox::create_sandboxed_lua()?;
let Some(filepath) = args.filepath else {
return repl::run(&lua).await;
};
let source = tokio::fs::read_to_string(&filepath).await?;
// Skip a leading shebang line like stock Lua's luaL_loadfilex does (it // Skip a leading shebang line like stock Lua's luaL_loadfilex does (it
// skips any first line starting with '#'). The newline is kept so error // skips any first line starting with '#'). The newline is kept so error
@ -30,11 +38,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
source.as_str() source.as_str()
}; };
let lua = sandbox::create_sandboxed_lua()?;
lua.load(chunk) lua.load(chunk)
// "@" marks the chunk name as a file path in error messages / tracebacks // "@" marks the chunk name as a file path in error messages / tracebacks
.set_name(format!("@{}", args.filepath.display())) .set_name(format!("@{}", filepath.display()))
.exec_async() .exec_async()
.await?; .await?;

@ -0,0 +1,145 @@
//! Interactive read-eval-print loop.
//!
//! Started when `a` is run with no script path. Each line is compiled and run in
//! the same sandboxed Lua state used for scripts, so the full stdlib (including
//! the async http/sqlite/os.sleep calls) is available. Evaluation uses the async
//! executor, matching how files are run.
use mlua::prelude::{LuaFunction, LuaMultiValue, LuaTable};
use mlua::Lua;
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
const PROMPT: &str = "a> ";
const CONTINUATION_PROMPT: &str = "..> ";
/// Outcome of trying to compile a piece of REPL input.
enum Compiled {
/// A ready-to-run chunk.
Chunk(LuaFunction),
/// Input is syntactically incomplete; more lines are needed.
Incomplete,
/// Input is invalid and cannot become valid by appending more lines.
Error(mlua::Error),
}
/// Compile a complete piece of REPL input.
///
/// Mirrors stock Lua's `lua.c`: first try the input wrapped in `return ( ... )`
/// so that bare expressions print their value, and fall back to running it as a
/// statement. Only the statement form's error decides whether the input is
/// merely incomplete (so a half-typed expression keeps reading more lines).
fn compile(lua: &Lua, source: &str) -> Compiled {
if let Ok(func) = lua
.load(format!("return {source}"))
.set_name("=stdin")
.into_function()
{
return Compiled::Chunk(func);
}
match lua.load(source).set_name("=stdin").into_function() {
Ok(func) => Compiled::Chunk(func),
Err(mlua::Error::SyntaxError {
incomplete_input: true,
..
}) => Compiled::Incomplete,
Err(err) => Compiled::Error(err),
}
}
/// Pretty-print the results of an evaluated chunk, one per line, using the
/// project's own `utils.dump`. Nothing is printed when the chunk returns no
/// values (e.g. a plain statement).
fn print_results(lua: &Lua, values: LuaMultiValue) -> mlua::Result<()> {
if values.is_empty() {
return Ok(());
}
let dump: LuaFunction = lua
.globals()
.get::<LuaTable>("utils")?
.get::<LuaFunction>("dump")?;
for value in values {
let rendered: String = dump.call(value)?;
println!("{rendered}");
}
Ok(())
}
/// Run the interactive REPL against an already-sandboxed Lua state.
pub(crate) async fn run(lua: &Lua) -> Result<(), Box<dyn std::error::Error>> {
let mut editor = DefaultEditor::new()?;
let history_path = std::env::var_os("HOME").map(|home| {
let mut p = std::path::PathBuf::from(home);
p.push(".a_history");
p
});
if let Some(path) = &history_path {
// A missing history file on first run is not an error.
let _ = editor.load_history(path);
}
println!(
"a {} — interactive Lua. Ctrl-D to exit.",
env!("CARGO_PKG_VERSION")
);
// Accumulated lines of an as-yet-incomplete statement.
let mut buffer = String::new();
loop {
let prompt = if buffer.is_empty() {
PROMPT
} else {
CONTINUATION_PROMPT
};
match editor.readline(prompt) {
Ok(line) => {
if buffer.is_empty() {
buffer = line;
} else {
buffer.push('\n');
buffer.push_str(&line);
}
match compile(lua, &buffer) {
Compiled::Incomplete => continue, // keep reading continuation lines
Compiled::Chunk(func) => {
let _ = editor.add_history_entry(buffer.as_str());
match func.call_async::<LuaMultiValue>(()).await {
Ok(values) => {
if let Err(err) = print_results(lua, values) {
eprintln!("{err}");
}
}
Err(err) => eprintln!("{err}"),
}
buffer.clear();
}
Compiled::Error(err) => {
let _ = editor.add_history_entry(buffer.as_str());
eprintln!("{err}");
buffer.clear();
}
}
}
// Ctrl-C: abandon the current input and start fresh.
Err(ReadlineError::Interrupted) => {
buffer.clear();
}
// Ctrl-D (on an empty line) or end of piped input: quit.
Err(ReadlineError::Eof) => break,
Err(err) => {
eprintln!("{err}");
break;
}
}
}
if let Some(path) = &history_path {
let _ = editor.save_history(path);
}
Ok(())
}
Loading…
Cancel
Save