diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock index 9ad121371..fa2c2c3dc 100644 --- a/tvix/Cargo.lock +++ b/tvix/Cargo.lock @@ -4119,6 +4119,7 @@ dependencies = [ "clap", "dirs", "nix-compat", + "rnix", "rustyline", "thiserror", "tokio", diff --git a/tvix/Cargo.nix b/tvix/Cargo.nix index f56618114..1addf005d 100644 --- a/tvix/Cargo.nix +++ b/tvix/Cargo.nix @@ -13042,6 +13042,10 @@ rec { name = "nix-compat"; packageId = "nix-compat"; } + { + name = "rnix"; + packageId = "rnix"; + } { name = "rustyline"; packageId = "rustyline"; diff --git a/tvix/cli/Cargo.toml b/tvix/cli/Cargo.toml index 1fa235182..be85597fa 100644 --- a/tvix/cli/Cargo.toml +++ b/tvix/cli/Cargo.toml @@ -18,6 +18,7 @@ bytes = "1.4.0" clap = { version = "4.0", features = ["derive", "env"] } dirs = "4.0.0" rustyline = "10.0.0" +rnix = "0.11.0" thiserror = "1.0.38" tokio = "1.28.0" tracing = { version = "0.1.37", features = ["max_level_trace", "release_max_level_info"] } diff --git a/tvix/cli/src/main.rs b/tvix/cli/src/main.rs index d66d2ce4c..20f6d8fe9 100644 --- a/tvix/cli/src/main.rs +++ b/tvix/cli/src/main.rs @@ -9,7 +9,7 @@ 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; @@ -123,6 +123,22 @@ fn init_io_handle(tokio_runtime: &tokio::runtime::Runtime, args: &Args) -> Rc 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. @@ -132,7 +148,8 @@ fn interpret( path: Option, args: &Args, explain: bool, -) -> bool { + allow_incomplete: AllowIncomplete, +) -> Result { let mut eval = tvix_eval::Evaluation::new( Box::new(TvixIO::new(tvix_store_io.clone() as Rc)) as Box, true, @@ -163,6 +180,18 @@ fn interpret( 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)); @@ -188,7 +217,7 @@ fn interpret( } // 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 @@ -257,7 +286,16 @@ fn main() { if let Some(file) = &args.script { run_file(io_handle, file.clone(), &args) } else if let Some(expr) = &args.expr { - if !interpret(io_handle, expr, None, &args, false) { + if !interpret( + io_handle, + expr, + None, + &args, + false, + AllowIncomplete::RequireComplete, + ) + .unwrap() + { std::process::exit(1); } } else { @@ -274,7 +312,15 @@ fn run_file(io_handle: Rc, mut path: PathBuf, args: &Args) { let success = if args.compile_only { lint(&contents, Some(path), args) } else { - interpret(io_handle, &contents, Some(path), args, false) + interpret( + io_handle, + &contents, + Some(path), + args, + false, + AllowIncomplete::RequireComplete, + ) + .unwrap() }; if !success { @@ -318,20 +364,59 @@ fn run_prompt(io_handle: Rc, args: &Args) { None => None, }; + let mut multiline_input: Option = None; loop { - let readline = rl.readline("tvix-repl> "); + let prompt = if multiline_input.is_some() { + " > " + } else { + "tvix-repl> " + }; + + let readline = rl.readline(prompt); match readline { Ok(line) => { if line.is_empty() { continue; } - rl.add_history_entry(&line); - - if let Some(without_prefix) = line.strip_prefix(":d ") { - interpret(Rc::clone(&io_handle), without_prefix, None, args, true); + let input = if let Some(mi) = &mut multiline_input { + mi.push('\n'); + mi.push_str(&line); + mi } else { - interpret(Rc::clone(&io_handle), &line, None, args, false); + &line + }; + + let res = if let Some(without_prefix) = input.strip_prefix(":d ") { + interpret( + Rc::clone(&io_handle), + without_prefix, + None, + args, + true, + AllowIncomplete::Allow, + ) + } else { + interpret( + Rc::clone(&io_handle), + input, + None, + args, + false, + AllowIncomplete::Allow, + ) + }; + + match res { + Ok(_) => { + rl.add_history_entry(input); + multiline_input = None; + } + Err(IncompleteInput) => { + if multiline_input.is_none() { + multiline_input = Some(line); + } + } } } Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break,