mod build_info;
mod checker;
mod interpreter;
mod loader;
mod supported;
mod update;
use std::env;
use std::fs;
use std::path::Path;
use std::process::{Command, exit};
use anyhow::{Error, Result, anyhow, bail};
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
fn main() {
if let Err(e) = real_main() {
if let Some(p) = e.downcast_ref::<interpreter::ScriptPanic>() {
if p.file.is_empty() {
eprintln!("thread 'main' panicked:");
} else {
eprintln!("thread 'main' panicked at {}:{}:", p.file, p.line);
}
eprintln!("{}", p.rendered);
exit(101);
}
if let Some(r) = e.downcast_ref::<interpreter::ErrReturn>() {
eprintln!("Error: {}", r.0);
exit(1);
}
let rendered = format!("{e:#}");
if rendered.contains("unsupported")
|| rendered.contains("not supported")
|| rendered.contains("not implemented by")
{
eprintln!("rust unsupported: {rendered}");
} else {
eprintln!("rust error: {rendered}");
}
exit(1);
}
}
fn real_main() -> Result<()> {
let all: Vec<String> = env::args().skip(1).collect();
let cmd = all.first().cloned().unwrap_or_default();
match cmd.as_str() {
"run" => {
let file = all.get(1).ok_or_else(err_usage)?;
run(file, &all[2..])
}
"check" => {
let file = all.get(1).ok_or_else(err_usage)?;
let source = fs::read_to_string(file)?;
let program = loader::load(Path::new(file), &source)?;
checker::check(Path::new(file), &program.files, &program.crate_deps)?;
check_coverage(&program)?;
println!("ok");
Ok(())
}
"build" => {
let file = all.get(1).ok_or_else(err_usage)?;
build_run(file, &all[2..])
}
"clean" => checker::clean(),
"update" => update::update(&all[1..]),
"supported" => {
if all.get(1).map(String::as_str) == Some("md") {
print!("{}", supported::markdown());
} else {
supported::print_supported();
}
Ok(())
}
"-e" => {
let code = all
.get(1)
.ok_or_else(|| anyhow!("missing code after -e, try `rust help`"))?;
eval(code, &all[2..])
}
"-V" | "--version" => {
println!("{}", build_info::version());
Ok(())
}
"-h" | "--help" | "help" | "" => {
print_usage();
Ok(())
}
path if path.ends_with(".rs") || Path::new(path).is_file() => run(path, &all[1..]),
other => bail!("unknown command `{other}`, try `rust help`"),
}
}
fn check_coverage(program: &loader::Program) -> Result<()> {
let engine = if program.tokio_main {
interpreter::coverage::Engine::Parallel
} else {
interpreter::coverage::Engine::Fast
};
let interp = interpreter::Interp::load(&program.modules, program.tokio_main)?;
let findings = interp.coverage(engine);
if findings.is_empty() {
return Ok(());
}
let mut out = String::new();
for finding in &findings {
out.push_str(" ");
out.push_str(&finding.message());
out.push('\n');
}
let engine_name = if program.tokio_main {
"the parallel engine, which #[tokio::main] selects"
} else {
"the interpreter"
};
let (count, verb) = if findings.len() == 1 {
("1 method".to_string(), "is")
} else {
(format!("{} methods", findings.len()), "are")
};
Err(anyhow!(
"{count} used by this script {verb} not implemented by {engine_name}:\n{}",
out.trim_end()
))
}
fn run(file: &str, script_args: &[String]) -> Result<()> {
if script_args.first().is_some_and(|a| a == "cmp") {
return build_run(file, &script_args[1..]);
}
let path = Path::new(file)
.canonicalize()
.unwrap_or_else(|_| Path::new(file).to_path_buf());
let source = fs::read_to_string(&path).map_err(|e| anyhow!("cannot read {file}: {e}"))?;
let program = loader::load(&path, &source)?;
let mut args = vec![file.to_string()];
args.extend(script_args.iter().cloned());
interpreter::set_script_args(args);
if program.tokio_main {
return interpreter::run_parallel(&program.modules);
}
let interp = interpreter::Interp::load(&program.modules, false)?;
interp.run_main()
}
fn eval(code: &str, script_args: &[String]) -> Result<()> {
let source = if is_program(code) {
code.to_string()
} else {
format!("fn main() {{ {code}\n}}\n")
};
let dir = env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
let program = loader::load(&dir.join("-e.rs"), &source)?;
let mut args = vec!["-e".to_string()];
args.extend(script_args.iter().cloned());
interpreter::set_script_args(args);
if program.tokio_main {
return interpreter::run_parallel(&program.modules);
}
let interp = interpreter::Interp::load(&program.modules, false)?;
interp.run_main()
}
fn is_program(code: &str) -> bool {
let Ok(ast) = syn::parse_file(code) else {
return false;
};
ast.items
.iter()
.any(|item| matches!(item, syn::Item::Fn(f) if f.sig.ident == "main"))
}
fn build_run(file: &str, script_args: &[String]) -> Result<()> {
let path = Path::new(file)
.canonicalize()
.unwrap_or_else(|_| Path::new(file).to_path_buf());
let source = fs::read_to_string(&path).map_err(|e| anyhow!("cannot read {file}: {e}"))?;
let program = loader::load(&path, &source)?;
let bin = checker::build(&path, &program.files, &program.crate_deps)?;
let status = Command::new(&bin)
.args(script_args)
.status()
.map_err(|e| anyhow!("cannot run compiled binary {}: {e}", bin.display()))?;
exit(status.code().unwrap_or(1));
}
fn err_usage() -> Error {
anyhow!("missing file argument, try `rust help`")
}
fn print_usage() {
println!(
r"rust - run a subset of Rust as a script
usage:
rust run FILE.rs interpret the script
rust FILE.rs same as run
rust -e 'CODE' run a snippet, arguments after CODE go to it
rust FILE.rs cmp compile and run, `cmp` first arg is reserved
rust build FILE.rs compile to a native binary, cache it, then run
rust check FILE.rs validate with cargo check, does not run
rust supported list every bridged method per receiver and engine
rust clean clear the cache
rust update [VER] install a prebuilt release, the newest one by default,
--from-source builds it with cargo instead
rust --version show version and build information
rust help show this help"
);
}