use clap::{Parser, Subcommand};
use colored::Colorize;
use std::io;
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Parser, Debug)]
#[command(name = "vudo")]
#[command(author, version, about = "VUDO Spirit runtime and toolchain")]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(short, long, global = true)]
quiet: bool,
}
#[derive(Subcommand, Debug)]
enum Commands {
Run(RunArgs),
Compile(CompileArgs),
Check(CheckArgs),
Repl(ReplArgs),
}
#[derive(Parser, Debug)]
struct RunArgs {
#[arg(required = true)]
file: PathBuf,
#[arg(short, long)]
function: Option<String>,
#[arg(short, long)]
args: Option<String>,
#[arg(long, default_value = "16")]
memory: u32,
#[arg(long)]
trace: bool,
}
#[derive(Parser, Debug)]
struct CompileArgs {
#[arg(required = true)]
file: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
optimize: bool,
#[arg(long)]
debug: bool,
}
#[derive(Parser, Debug)]
struct CheckArgs {
#[arg(required = true)]
paths: Vec<PathBuf>,
#[arg(long)]
strict: bool,
#[arg(long)]
json: bool,
#[arg(short, long)]
recursive: bool,
}
#[derive(Parser, Debug)]
struct ReplArgs {
#[arg(short, long)]
load: Option<PathBuf>,
#[arg(long, default_value = "true")]
tree_shake: bool,
#[arg(long)]
optimize: bool,
#[arg(long, default_value = "default")]
session: String,
}
fn main() -> ExitCode {
let cli = Cli::parse();
let result = match cli.command {
Commands::Run(args) => cmd_run(args, cli.verbose, cli.quiet),
Commands::Compile(args) => cmd_compile(args, cli.verbose, cli.quiet),
Commands::Check(args) => cmd_check(args, cli.verbose, cli.quiet),
Commands::Repl(args) => cmd_repl(args, cli.verbose, cli.quiet),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{}: {}", "error".red(), e);
ExitCode::FAILURE
}
}
}
#[cfg(feature = "wasm")]
fn cmd_run(args: RunArgs, verbose: bool, quiet: bool) -> Result<(), String> {
use wasmtime::{Engine, Instance, Module, Store, Val};
if !args.file.exists() {
return Err(format!("File not found: {}", args.file.display()));
}
if verbose {
eprintln!("{} {}", "Loading".cyan(), args.file.display());
}
let wasm_bytes =
std::fs::read(&args.file).map_err(|e| format!("Failed to read WASM file: {}", e))?;
if verbose {
eprintln!(" {} bytes loaded", wasm_bytes.len());
}
let engine = Engine::default();
let module =
Module::new(&engine, &wasm_bytes).map_err(|e| format!("Failed to compile WASM: {}", e))?;
let mut store = Store::new(&engine, ());
let instance = Instance::new(&mut store, &module, &[])
.map_err(|e| format!("Failed to instantiate WASM: {}", e))?;
let func_name = args.function.as_deref().unwrap_or_else(|| {
if instance.get_func(&mut store, "main").is_some() {
"main"
} else {
module
.exports()
.find(|e| e.ty().func().is_some())
.map(|e| e.name())
.unwrap_or("main")
}
});
let func = instance
.get_func(&mut store, func_name)
.ok_or_else(|| format!("Function '{}' not found", func_name))?;
if verbose {
eprintln!("{} {}()", "Calling".cyan(), func_name);
}
let call_args = parse_json_args(&args.args, &func, &store)?;
let func_ty = func.ty(&store);
let mut results: Vec<Val> = func_ty.results().map(|_| Val::I64(0)).collect();
func.call(&mut store, &call_args, &mut results)
.map_err(|e| format!("Execution error: {}", e))?;
if !quiet {
if results.is_empty() {
println!("(no return value)");
} else if results.len() == 1 {
println!("{}", format_val(&results[0]));
} else {
let formatted: Vec<String> = results.iter().map(format_val).collect();
println!("({})", formatted.join(", "));
}
}
Ok(())
}
#[cfg(feature = "wasm")]
fn parse_json_args(
args_json: &Option<String>,
func: &wasmtime::Func,
store: &wasmtime::Store<()>,
) -> Result<Vec<wasmtime::Val>, String> {
use wasmtime::{Val, ValType};
let Some(json_str) = args_json else {
return Ok(vec![]);
};
let parsed: serde_json::Value =
serde_json::from_str(json_str).map_err(|e| format!("Invalid JSON arguments: {}", e))?;
let arr = parsed
.as_array()
.ok_or_else(|| "Arguments must be a JSON array".to_string())?;
let func_ty = func.ty(store);
let param_types: Vec<_> = func_ty.params().collect();
if arr.len() != param_types.len() {
return Err(format!(
"Expected {} arguments, got {}",
param_types.len(),
arr.len()
));
}
let mut vals = Vec::with_capacity(arr.len());
for (i, (val, ty)) in arr.iter().zip(param_types.iter()).enumerate() {
let wasm_val = match ty {
ValType::I32 => {
let n = val
.as_i64()
.ok_or_else(|| format!("Argument {} must be an integer", i))?;
Val::I32(n as i32)
}
ValType::I64 => {
let n = val
.as_i64()
.ok_or_else(|| format!("Argument {} must be an integer", i))?;
Val::I64(n)
}
ValType::F32 => {
let n = val
.as_f64()
.ok_or_else(|| format!("Argument {} must be a number", i))?;
Val::F32((n as f32).to_bits())
}
ValType::F64 => {
let n = val
.as_f64()
.ok_or_else(|| format!("Argument {} must be a number", i))?;
Val::F64(n.to_bits())
}
_ => return Err(format!("Unsupported parameter type at position {}", i)),
};
vals.push(wasm_val);
}
Ok(vals)
}
#[cfg(feature = "wasm")]
fn format_val(val: &wasmtime::Val) -> String {
match val {
wasmtime::Val::I32(n) => n.to_string(),
wasmtime::Val::I64(n) => n.to_string(),
wasmtime::Val::F32(bits) => f32::from_bits(*bits).to_string(),
wasmtime::Val::F64(bits) => f64::from_bits(*bits).to_string(),
_ => format!("{:?}", val),
}
}
#[cfg(not(feature = "wasm"))]
fn cmd_run(_args: RunArgs, _verbose: bool, _quiet: bool) -> Result<(), String> {
Err("WASM feature not enabled. Rebuild with --features wasm".to_string())
}
#[cfg(feature = "wasm")]
fn cmd_compile(args: CompileArgs, verbose: bool, quiet: bool) -> Result<(), String> {
use metadol::parse_dol_file;
use metadol::wasm::WasmCompiler;
if !args.file.exists() {
return Err(format!("File not found: {}", args.file.display()));
}
if verbose {
eprintln!("{} {}", "Compiling".cyan(), args.file.display());
}
let source =
std::fs::read_to_string(&args.file).map_err(|e| format!("Failed to read file: {}", e))?;
let file = parse_dol_file(&source).map_err(|e| format!("Parse error: {:?}", e))?;
if verbose {
eprintln!(" Parsed {} declarations", file.declarations.len());
}
let mut compiler = WasmCompiler::new();
if args.optimize {
compiler = compiler.with_optimization(true);
}
let wasm_bytes = compiler
.compile_file(&file)
.map_err(|e| format!("Compile error: {}", e.message))?;
if verbose {
eprintln!(" Generated {} bytes of WASM", wasm_bytes.len());
}
let output_path = args
.output
.unwrap_or_else(|| args.file.with_extension("wasm"));
std::fs::write(&output_path, &wasm_bytes)
.map_err(|e| format!("Failed to write output: {}", e))?;
if !quiet {
eprintln!(
"{} {} ({} bytes)",
"Wrote".green(),
output_path.display(),
wasm_bytes.len()
);
}
Ok(())
}
#[cfg(not(feature = "wasm"))]
fn cmd_compile(_args: CompileArgs, _verbose: bool, _quiet: bool) -> Result<(), String> {
Err("WASM feature not enabled. Rebuild with --features wasm".to_string())
}
fn cmd_check(args: CheckArgs, verbose: bool, quiet: bool) -> Result<(), String> {
let files = collect_dol_files(&args.paths, args.recursive);
if files.is_empty() {
if !quiet {
eprintln!("{}: No .dol files found", "warning".yellow());
}
return Ok(());
}
if verbose {
eprintln!("Checking {} file(s)...", files.len());
}
let mut passed = 0;
let mut failed = 0;
let mut errors: Vec<(PathBuf, String)> = Vec::new();
for path in &files {
match check_file(path) {
Ok(()) => {
passed += 1;
if verbose {
eprintln!("{} {}", " OK".green(), path.display());
}
}
Err(e) => {
failed += 1;
if !args.json {
eprintln!("{} {}: {}", "FAIL".red(), path.display(), e);
}
errors.push((path.clone(), e));
}
}
}
if args.json {
let result = serde_json::json!({
"passed": passed,
"failed": failed,
"errors": errors.iter().map(|(p, e)| {
serde_json::json!({
"file": p.display().to_string(),
"error": e
})
}).collect::<Vec<_>>()
});
println!("{}", serde_json::to_string_pretty(&result).unwrap());
} else if !quiet {
eprintln!(
"\n{} passed, {} failed",
passed.to_string().green(),
if failed > 0 {
failed.to_string().red()
} else {
failed.to_string().normal()
}
);
}
if failed > 0 && args.strict {
Err(format!("{} file(s) failed type check", failed))
} else if failed > 0 {
Ok(())
} else {
Ok(())
}
}
fn check_file(path: &PathBuf) -> Result<(), String> {
let source =
std::fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;
metadol::parse_file(&source).map_err(|e| format!("Parse error: {}", e))?;
Ok(())
}
fn cmd_repl(args: ReplArgs, verbose: bool, _quiet: bool) -> Result<(), String> {
use metadol::repl::{EvalResult, SessionConfig, SpiritRepl};
use std::io::{self, BufRead, Write};
let config = SessionConfig::with_name(&args.session)
.with_tree_shaking(args.tree_shake)
.with_optimization(args.optimize);
let mut repl = SpiritRepl::with_config(config);
if let Some(path) = &args.load {
if verbose {
eprintln!("{} {}", "Loading".cyan(), path.display());
}
let source =
std::fs::read_to_string(path).map_err(|e| format!("Failed to load file: {}", e))?;
let file =
metadol::parse_dol_file(&source).map_err(|e| format!("Failed to parse file: {}", e))?;
for _decl in file.declarations {
let _ = repl.eval("// loaded from file");
}
eprintln!("Loaded {}", path.display());
}
println!("{}", "Spirit REPL v0.8.0".cyan().bold());
println!(
"Type {} for help, {} to quit",
":help".green(),
":quit".green()
);
println!();
let stdin = io::stdin();
let mut stdout = io::stdout();
loop {
print!("{} ", "dol>".blue().bold());
stdout.flush().map_err(|e| e.to_string())?;
let mut line = String::new();
if stdin
.lock()
.read_line(&mut line)
.map_err(|e| e.to_string())?
== 0
{
println!();
break;
}
let input = line.trim();
let full_input = if needs_continuation(input) {
collect_multiline(&stdin, input)?
} else {
input.to_string()
};
match repl.eval(&full_input) {
Ok(result) => match result {
EvalResult::Empty => {}
EvalResult::Quit => {
println!("Goodbye!");
break;
}
EvalResult::Help(text) => println!("{}", text),
EvalResult::Message(msg) => println!("{}", msg),
EvalResult::Defined {
name,
kind,
message,
} => {
println!(
"{} {} {}: {}",
"Defined".green(),
kind.cyan(),
name.yellow(),
message
);
}
EvalResult::Expression { value, .. } => {
println!("= {}", value.yellow());
}
EvalResult::TypeInfo(info) => {
println!("{}", info.cyan());
}
EvalResult::RustCode(code) => {
println!("--- Rust ---");
println!("{}", code);
println!("------------");
}
EvalResult::WasmInfo {
size_bytes,
functions,
has_memory,
} => {
println!(
"WASM: {} bytes, {} functions, memory: {}",
size_bytes, functions, has_memory
);
}
},
Err(e) => {
eprintln!("{}: {}", "Error".red(), e);
}
}
}
if verbose {
println!(
"\nSession ended. {} declarations defined.",
repl.declarations().len()
);
}
Ok(())
}
fn needs_continuation(input: &str) -> bool {
let open_braces = input.matches('{').count();
let close_braces = input.matches('}').count();
open_braces > close_braces
}
fn collect_multiline(stdin: &io::Stdin, first_line: &str) -> Result<String, String> {
use std::io::{BufRead, Write};
let mut full = first_line.to_string();
let mut stdout = io::stdout();
loop {
print!("{} ", "...".blue());
stdout.flush().map_err(|e| e.to_string())?;
let mut line = String::new();
if stdin
.lock()
.read_line(&mut line)
.map_err(|e| e.to_string())?
== 0
{
break;
}
full.push('\n');
full.push_str(&line);
let open_braces = full.matches('{').count();
let close_braces = full.matches('}').count();
if open_braces <= close_braces {
break;
}
}
Ok(full)
}
fn collect_dol_files(paths: &[PathBuf], recursive: bool) -> Vec<PathBuf> {
let mut files = Vec::new();
for path in paths {
if path.is_file() {
if path.extension().is_some_and(|ext| ext == "dol") {
files.push(path.clone());
}
} else if path.is_dir() {
if recursive {
collect_dol_files_recursive(path, &mut files);
} else {
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_file() && p.extension().is_some_and(|ext| ext == "dol") {
files.push(p);
}
}
}
}
}
}
files.sort();
files
}
fn collect_dol_files_recursive(dir: &PathBuf, files: &mut Vec<PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_dol_files_recursive(&path, files);
} else if path.extension().is_some_and(|ext| ext == "dol") {
files.push(path);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_empty() {
let files = collect_dol_files(&[], false);
assert!(files.is_empty());
}
}