From 96ce9aea046b584c411afdc210e73764f27ff202 Mon Sep 17 00:00:00 2001 From: Aspen Smith Date: Mon, 27 May 2024 14:15:38 -0400 Subject: feat(tvix/repl): Support multiline input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transparently support multiline input in the Tvix REPL, by handling the UnexpectedEOF error returned by the parser and using it to progressively build up an input expr over multiple iterations of the REPL's outer loop. This works quite nicely: ❯ cargo r --bin tvix Compiling tvix-cli v0.1.0 (/home/aspen/code/depot/tvix/cli) Finished dev [unoptimized + debuginfo] target(s) in 1.72s Running `target/debug/tvix` tvix-repl> { foo > = > 1; > } => { foo = 1; } :: set tvix-repl> { foo = 1; } => { foo = 1; } :: set Change-Id: Ib0ed4766b13e8231d696cdc27281ac158e20a777 Reviewed-on: https://cl.tvl.fyi/c/depot/+/11732 Reviewed-by: tazjin Autosubmit: aspen Reviewed-by: flokli Tested-by: BuildkiteCI --- tvix/Cargo.lock | 1 + tvix/Cargo.nix | 4 ++ tvix/cli/Cargo.toml | 1 + tvix/cli/src/main.rs | 107 +++++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock index 9ad121371264..fa2c2c3dcc52 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 f56618114031..1addf005dea9 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 1fa23518228a..be85597fa2ae 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 d66d2ce4cbb6..20f6d8fe9774 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 { + &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), &line, None, args, false); + 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, -- cgit 1.4.1