diff options
Diffstat (limited to 'tvix/cli/src')
-rw-r--r-- | tvix/cli/src/main.rs | 192 | ||||
-rw-r--r-- | tvix/cli/src/repl.rs | 175 |
2 files changed, 276 insertions, 91 deletions
diff --git a/tvix/cli/src/main.rs b/tvix/cli/src/main.rs index 436e895863..292a223cbb 100644 --- a/tvix/cli/src/main.rs +++ b/tvix/cli/src/main.rs @@ -1,21 +1,24 @@ +mod repl; + use clap::Parser; -use rustyline::{error::ReadlineError, Editor}; +use repl::Repl; use std::rc::Rc; use std::{fs, path::PathBuf}; use tracing::Level; use tracing_subscriber::fmt::writer::MakeWriterExt; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::{EnvFilter, Layer}; use tvix_build::buildservice; use tvix_eval::builtins::impure_builtins; use tvix_eval::observer::{DisassemblingObserver, TracingObserver}; -use tvix_eval::{EvalIO, Value}; +use tvix_eval::{ErrorKind, EvalIO, Value}; use tvix_glue::builtins::add_fetcher_builtins; use tvix_glue::builtins::add_import_builtins; use tvix_glue::tvix_io::TvixIO; use tvix_glue::tvix_store_io::TvixStoreIO; use tvix_glue::{builtins::add_derivation_builtins, configure_nix_path}; -#[derive(Parser)] +#[derive(Parser, Clone)] struct Args { #[arg(long)] log_level: Option<Level>, @@ -79,27 +82,23 @@ struct Args { build_service_addr: String, } -/// Interprets the given code snippet, printing out warnings, errors -/// and the result itself. The return value indicates whether -/// evaluation succeeded. -fn interpret(code: &str, path: Option<PathBuf>, args: &Args, explain: bool) -> bool { - let tokio_runtime = tokio::runtime::Runtime::new().expect("failed to setup tokio runtime"); - - let (blob_service, directory_service, path_info_service) = tokio_runtime - .block_on({ - let blob_service_addr = args.blob_service_addr.clone(); - let directory_service_addr = args.directory_service_addr.clone(); - let path_info_service_addr = args.path_info_service_addr.clone(); - async move { - tvix_store::utils::construct_services( - blob_service_addr, - directory_service_addr, - path_info_service_addr, - ) - .await - } - }) - .expect("unable to setup {blob|directory|pathinfo}service before interpreter setup"); +fn init_io_handle(tokio_runtime: &tokio::runtime::Runtime, args: &Args) -> Rc<TvixStoreIO> { + let (blob_service, directory_service, path_info_service, nar_calculation_service) = + tokio_runtime + .block_on({ + let blob_service_addr = args.blob_service_addr.clone(); + let directory_service_addr = args.directory_service_addr.clone(); + let path_info_service_addr = args.path_info_service_addr.clone(); + async move { + tvix_store::utils::construct_services( + blob_service_addr, + directory_service_addr, + path_info_service_addr, + ) + .await + } + }) + .expect("unable to setup {blob|directory|pathinfo}service before interpreter setup"); let build_service = tokio_runtime .block_on({ @@ -116,14 +115,43 @@ fn interpret(code: &str, path: Option<PathBuf>, args: &Args, explain: bool) -> b }) .expect("unable to setup buildservice before interpreter setup"); - let tvix_store_io = Rc::new(TvixStoreIO::new( + Rc::new(TvixStoreIO::new( blob_service.clone(), directory_service.clone(), path_info_service.into(), + nar_calculation_service.into(), build_service.into(), tokio_runtime.handle().clone(), - )); + )) +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +enum AllowIncomplete { + Allow, + #[default] + RequireComplete, +} + +impl AllowIncomplete { + fn allow(&self) -> bool { + matches!(self, Self::Allow) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct IncompleteInput; + +/// Interprets the given code snippet, printing out warnings, errors +/// and the result itself. The return value indicates whether +/// evaluation succeeded. +fn interpret( + tvix_store_io: Rc<TvixStoreIO>, + code: &str, + path: Option<PathBuf>, + args: &Args, + explain: bool, + allow_incomplete: AllowIncomplete, +) -> Result<bool, IncompleteInput> { let mut eval = tvix_eval::Evaluation::new( Box::new(TvixIO::new(tvix_store_io.clone() as Rc<dyn EvalIO>)) as Box<dyn EvalIO>, true, @@ -154,6 +182,18 @@ fn interpret(code: &str, path: Option<PathBuf>, args: &Args, explain: bool) -> b eval.evaluate(code, path) }; + if allow_incomplete.allow() + && result.errors.iter().any(|err| { + matches!( + &err.kind, + ErrorKind::ParseErrors(pes) + if pes.iter().any(|pe| matches!(pe, rnix::parser::ParseError::UnexpectedEOF)) + ) + }) + { + return Err(IncompleteInput); + } + if args.display_ast { if let Some(ref expr) = result.expr { eprintln!("AST: {}", tvix_eval::pretty_print_expr(expr)); @@ -179,7 +219,7 @@ fn interpret(code: &str, path: Option<PathBuf>, args: &Args, explain: bool) -> b } // inform the caller about any errors - result.errors.is_empty() + Ok(result.errors.is_empty()) } /// Interpret the given code snippet, but only run the Tvix compiler @@ -229,24 +269,44 @@ fn main() { let subscriber = tracing_subscriber::registry().with( tracing_subscriber::fmt::Layer::new() .with_writer(std::io::stderr.with_max_level(level)) - .pretty(), + .compact() + .with_filter( + EnvFilter::builder() + .with_default_directive(level.into()) + .from_env() + .expect("invalid RUST_LOG"), + ), ); subscriber .try_init() .expect("unable to set up tracing subscriber"); + let tokio_runtime = tokio::runtime::Runtime::new().expect("failed to setup tokio runtime"); + + let io_handle = init_io_handle(&tokio_runtime, &args); + if let Some(file) = &args.script { - run_file(file.clone(), &args) + run_file(io_handle, file.clone(), &args) } else if let Some(expr) = &args.expr { - if !interpret(expr, None, &args, false) { + if !interpret( + io_handle, + expr, + None, + &args, + false, + AllowIncomplete::RequireComplete, + ) + .unwrap() + { std::process::exit(1); } } else { - run_prompt(&args) + let mut repl = Repl::new(); + repl.run(io_handle, &args) } } -fn run_file(mut path: PathBuf, args: &Args) { +fn run_file(io_handle: Rc<TvixStoreIO>, mut path: PathBuf, args: &Args) { if path.is_dir() { path.push("default.nix"); } @@ -255,7 +315,15 @@ fn run_file(mut path: PathBuf, args: &Args) { let success = if args.compile_only { lint(&contents, Some(path), args) } else { - interpret(&contents, Some(path), args, false) + interpret( + io_handle, + &contents, + Some(path), + args, + false, + AllowIncomplete::RequireComplete, + ) + .unwrap() }; if !success { @@ -270,61 +338,3 @@ fn println_result(result: &Value, raw: bool) { println!("=> {} :: {}", result, result.type_of()) } } - -fn state_dir() -> Option<PathBuf> { - let mut path = dirs::data_dir(); - if let Some(p) = path.as_mut() { - p.push("tvix") - } - path -} - -fn run_prompt(args: &Args) { - let mut rl = Editor::<()>::new().expect("should be able to launch rustyline"); - - if args.compile_only { - eprintln!("warning: `--compile-only` has no effect on REPL usage!"); - } - - let history_path = match state_dir() { - // Attempt to set up these paths, but do not hard fail if it - // doesn't work. - Some(mut path) => { - let _ = std::fs::create_dir_all(&path); - path.push("history.txt"); - let _ = rl.load_history(&path); - Some(path) - } - - None => None, - }; - - loop { - let readline = rl.readline("tvix-repl> "); - match readline { - Ok(line) => { - if line.is_empty() { - continue; - } - - rl.add_history_entry(&line); - - if let Some(without_prefix) = line.strip_prefix(":d ") { - interpret(without_prefix, None, args, true); - } else { - interpret(&line, None, args, false); - } - } - Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break, - - Err(err) => { - eprintln!("error: {}", err); - break; - } - } - } - - if let Some(path) = history_path { - rl.save_history(&path).unwrap(); - } -} diff --git a/tvix/cli/src/repl.rs b/tvix/cli/src/repl.rs new file mode 100644 index 0000000000..5a4830a027 --- /dev/null +++ b/tvix/cli/src/repl.rs @@ -0,0 +1,175 @@ +use std::path::PathBuf; +use std::rc::Rc; + +use rustyline::{error::ReadlineError, Editor}; +use tvix_glue::tvix_store_io::TvixStoreIO; + +use crate::{interpret, AllowIncomplete, Args, IncompleteInput}; + +fn state_dir() -> Option<PathBuf> { + let mut path = dirs::data_dir(); + if let Some(p) = path.as_mut() { + p.push("tvix") + } + path +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReplCommand<'a> { + Expr(&'a str), + Explain(&'a str), + Print(&'a str), + Quit, + Help, +} + +impl<'a> ReplCommand<'a> { + const HELP: &'static str = " +Welcome to the Tvix REPL! + +The following commands are supported: + + <expr> Evaluate a Nix language expression and print the result, along with its inferred type + :d <expr> Evaluate a Nix language expression and print a detailed description of the result + :p <expr> Evaluate a Nix language expression and print the result recursively + :q Exit the REPL + :?, :h Display this help text +"; + + pub fn parse(input: &'a str) -> Self { + if input.starts_with(':') { + if let Some(without_prefix) = input.strip_prefix(":d ") { + return Self::Explain(without_prefix); + } else if let Some(without_prefix) = input.strip_prefix(":p ") { + return Self::Print(without_prefix); + } + + let input = input.trim_end(); + match input { + ":q" => return Self::Quit, + ":h" | ":?" => return Self::Help, + _ => {} + } + } + + Self::Expr(input) + } +} + +#[derive(Debug)] +pub struct Repl { + /// In-progress multiline input, when the input so far doesn't parse as a complete expression + multiline_input: Option<String>, + rl: Editor<()>, +} + +impl Repl { + pub fn new() -> Self { + let rl = Editor::<()>::new().expect("should be able to launch rustyline"); + Self { + multiline_input: None, + rl, + } + } + + pub fn run(&mut self, io_handle: Rc<TvixStoreIO>, args: &Args) { + if args.compile_only { + eprintln!("warning: `--compile-only` has no effect on REPL usage!"); + } + + let history_path = match state_dir() { + // Attempt to set up these paths, but do not hard fail if it + // doesn't work. + Some(mut path) => { + let _ = std::fs::create_dir_all(&path); + path.push("history.txt"); + let _ = self.rl.load_history(&path); + Some(path) + } + + None => None, + }; + + loop { + let prompt = if self.multiline_input.is_some() { + " > " + } else { + "tvix-repl> " + }; + + let readline = self.rl.readline(prompt); + match readline { + Ok(line) => { + if line.is_empty() { + continue; + } + + let input = if let Some(mi) = &mut self.multiline_input { + mi.push('\n'); + mi.push_str(&line); + mi + } else { + &line + }; + + let res = match ReplCommand::parse(input) { + ReplCommand::Quit => break, + ReplCommand::Help => { + println!("{}", ReplCommand::HELP); + Ok(false) + } + ReplCommand::Expr(input) => interpret( + Rc::clone(&io_handle), + input, + None, + args, + false, + AllowIncomplete::Allow, + ), + ReplCommand::Explain(input) => interpret( + Rc::clone(&io_handle), + input, + None, + args, + true, + AllowIncomplete::Allow, + ), + ReplCommand::Print(input) => interpret( + Rc::clone(&io_handle), + input, + None, + &Args { + strict: true, + ..(args.clone()) + }, + false, + AllowIncomplete::Allow, + ), + }; + + match res { + Ok(_) => { + self.rl.add_history_entry(input); + self.multiline_input = None; + } + Err(IncompleteInput) => { + if self.multiline_input.is_none() { + self.multiline_input = Some(line); + } + } + } + } + Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break, + + Err(err) => { + eprintln!("error: {}", err); + break; + } + } + } + + if let Some(path) = history_path { + self.rl.save_history(&path).unwrap(); + } + } +} |