test(tvix/cli): Make the REPL testable

Juggle around the internals of the tvix-cli crate so that we expose the
Repl as a public type with a `send` method, that sends a string to the
repl and *captures all output* so that it can be subsequently asserted
on in tests. Then, demonstrate that this works with a single (for now)
REPL test using expect-test to assert on the output of a single command
sent to the REPL.

As the REPL gets more complicated, this will allow us to make tests that
cover that complex behavior.

Change-Id: I88175bd72d8760c79faade95ebb1d956f08a7b83
Reviewed-on: https://cl.tvl.fyi/c/depot/+/11958
Autosubmit: aspen <root@gws.fyi>
Tested-by: BuildkiteCI
Reviewed-by: flokli <flokli@flokli.de>
This commit is contained in:
Aspen Smith 2024-07-06 09:00:46 -04:00 committed by clbot
parent 3a79f93795
commit 0ad986169d
9 changed files with 522 additions and 346 deletions

17
tvix/Cargo.lock generated
View file

@ -976,6 +976,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "dissimilar"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d"
[[package]] [[package]]
name = "doc-comment" name = "doc-comment"
version = "0.3.3" version = "0.3.3"
@ -1136,6 +1142,16 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "expect-test"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e0be0a561335815e06dab7c62e50353134c796e7a6155402a64bcff66b6a5e0"
dependencies = [
"dissimilar",
"once_cell",
]
[[package]] [[package]]
name = "fastcdc" name = "fastcdc"
version = "3.1.0" version = "3.1.0"
@ -4365,6 +4381,7 @@ dependencies = [
"bytes", "bytes",
"clap", "clap",
"dirs", "dirs",
"expect-test",
"nix-compat", "nix-compat",
"rnix", "rnix",
"rowan", "rowan",

View file

@ -2965,6 +2965,16 @@ rec {
} }
]; ];
};
"dissimilar" = rec {
crateName = "dissimilar";
version = "1.0.9";
edition = "2018";
sha256 = "0bcn4s99ghigd3yadpd7i3gljv5z2hkr07ijvvxvsxmz3yfygy2r";
authors = [
"David Tolnay <dtolnay@gmail.com>"
];
}; };
"doc-comment" = rec { "doc-comment" = rec {
crateName = "doc-comment"; crateName = "doc-comment";
@ -3414,6 +3424,26 @@ rec {
}; };
resolvedDefaultFeatures = [ "std" ]; resolvedDefaultFeatures = [ "std" ];
}; };
"expect-test" = rec {
crateName = "expect-test";
version = "1.5.0";
edition = "2018";
sha256 = "1q55nrkgzg345905aqbsdrwlq4sk0gjn4z5bdph1an1kc6jy02wy";
authors = [
"rust-analyzer developers"
];
dependencies = [
{
name = "dissimilar";
packageId = "dissimilar";
}
{
name = "once_cell";
packageId = "once_cell";
}
];
};
"fastcdc" = rec { "fastcdc" = rec {
crateName = "fastcdc"; crateName = "fastcdc";
version = "3.1.0"; version = "3.1.0";
@ -13908,6 +13938,12 @@ rec {
packageId = "wu-manber"; packageId = "wu-manber";
} }
]; ];
devDependencies = [
{
name = "expect-test";
packageId = "expect-test";
}
];
features = { features = {
"tracy" = [ "tvix-tracing/tracy" ]; "tracy" = [ "tvix-tracing/tracy" ];
}; };

View file

@ -30,6 +30,9 @@ tracing-indicatif = "0.3.6"
[dependencies.wu-manber] [dependencies.wu-manber]
git = "https://github.com/tvlfyi/wu-manber.git" git = "https://github.com/tvlfyi/wu-manber.git"
[dev-dependencies]
expect-test = "1.5.0"
[target.'cfg(not(target_env = "msvc"))'.dependencies] [target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = "0.5" tikv-jemallocator = "0.5"

View file

@ -2,6 +2,9 @@
(depot.tvix.crates.workspaceMembers.tvix-cli.build.override { (depot.tvix.crates.workspaceMembers.tvix-cli.build.override {
runTests = true; runTests = true;
testPreRun = ''
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt;
'';
}).overrideAttrs (finalAttrs: previousAttrs: }).overrideAttrs (finalAttrs: previousAttrs:
let let

72
tvix/cli/src/args.rs Normal file
View file

@ -0,0 +1,72 @@
use std::path::PathBuf;
use clap::Parser;
use tracing::Level;
#[derive(Parser, Clone)]
pub struct Args {
/// A global log level to use when printing logs.
/// It's also possible to set `RUST_LOG` according to
/// `tracing_subscriber::filter::EnvFilter`, which will always have
/// priority.
#[arg(long, default_value_t=Level::INFO)]
pub log_level: Level,
/// Path to a script to evaluate
pub script: Option<PathBuf>,
#[clap(long, short = 'E')]
pub expr: Option<String>,
/// Dump the raw AST to stdout before interpreting
#[clap(long, env = "TVIX_DISPLAY_AST")]
pub display_ast: bool,
/// Dump the bytecode to stdout before evaluating
#[clap(long, env = "TVIX_DUMP_BYTECODE")]
pub dump_bytecode: bool,
/// Trace the runtime of the VM
#[clap(long, env = "TVIX_TRACE_RUNTIME")]
pub trace_runtime: bool,
/// Capture the time (relative to the start time of evaluation) of all events traced with
/// `--trace-runtime`
#[clap(long, env = "TVIX_TRACE_RUNTIME_TIMING", requires("trace_runtime"))]
pub trace_runtime_timing: bool,
/// Only compile, but do not execute code. This will make Tvix act
/// sort of like a linter.
#[clap(long)]
pub compile_only: bool,
/// Don't print warnings.
#[clap(long)]
pub no_warnings: bool,
/// A colon-separated list of directories to use to resolve `<...>`-style paths
#[clap(long, short = 'I', env = "NIX_PATH")]
pub nix_search_path: Option<String>,
/// Print "raw" (unquoted) output.
#[clap(long)]
pub raw: bool,
/// Strictly evaluate values, traversing them and forcing e.g.
/// elements of lists and attribute sets before printing the
/// return value.
#[clap(long)]
pub strict: bool,
#[arg(long, env, default_value = "memory://")]
pub blob_service_addr: String,
#[arg(long, env, default_value = "memory://")]
pub directory_service_addr: String,
#[arg(long, env, default_value = "memory://")]
pub path_info_service_addr: String,
#[arg(long, env, default_value = "dummy://")]
pub build_service_addr: String,
}

224
tvix/cli/src/lib.rs Normal file
View file

@ -0,0 +1,224 @@
use std::{collections::HashMap, path::PathBuf, rc::Rc};
use smol_str::SmolStr;
use std::fmt::Write;
use tracing::{instrument, Span};
use tracing_indicatif::span_ext::IndicatifSpanExt;
use tvix_build::buildservice;
use tvix_eval::{
builtins::impure_builtins,
observer::{DisassemblingObserver, TracingObserver},
ErrorKind, EvalIO, Value,
};
use tvix_glue::{
builtins::{add_derivation_builtins, add_fetcher_builtins, add_import_builtins},
configure_nix_path,
tvix_io::TvixIO,
tvix_store_io::TvixStoreIO,
};
pub mod args;
pub mod assignment;
pub mod repl;
pub use args::Args;
pub use repl::Repl;
pub 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({
let blob_service = blob_service.clone();
let directory_service = directory_service.clone();
async move {
buildservice::from_addr(
&args.build_service_addr,
blob_service.clone(),
directory_service.clone(),
)
.await
}
})
.expect("unable to setup buildservice before interpreter setup");
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)]
pub enum AllowIncomplete {
Allow,
#[default]
RequireComplete,
}
impl AllowIncomplete {
fn allow(&self) -> bool {
matches!(self, Self::Allow)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IncompleteInput;
/// Interprets the given code snippet, printing out warnings and errors and returning the result
pub fn evaluate(
tvix_store_io: Rc<TvixStoreIO>,
code: &str,
path: Option<PathBuf>,
args: &Args,
allow_incomplete: AllowIncomplete,
env: Option<&HashMap<SmolStr, Value>>,
) -> Result<Option<Value>, IncompleteInput> {
let span = Span::current();
span.pb_start();
span.pb_set_style(&tvix_tracing::PB_SPINNER_STYLE);
span.pb_set_message("Setting up evaluator…");
let mut eval_builder = tvix_eval::Evaluation::builder(Box::new(TvixIO::new(
tvix_store_io.clone() as Rc<dyn EvalIO>,
)) as Box<dyn EvalIO>)
.enable_import()
.with_strict(args.strict)
.add_builtins(impure_builtins())
.env(env);
eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&tvix_store_io));
eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&tvix_store_io));
eval_builder = add_import_builtins(eval_builder, tvix_store_io);
eval_builder = configure_nix_path(eval_builder, &args.nix_search_path);
let source_map = eval_builder.source_map().clone();
let result = {
let mut compiler_observer =
DisassemblingObserver::new(source_map.clone(), std::io::stderr());
if args.dump_bytecode {
eval_builder.set_compiler_observer(Some(&mut compiler_observer));
}
let mut runtime_observer = TracingObserver::new(std::io::stderr());
if args.trace_runtime {
if args.trace_runtime_timing {
runtime_observer.enable_timing()
}
eval_builder.set_runtime_observer(Some(&mut runtime_observer));
}
span.pb_set_message("Evaluating…");
let eval = eval_builder.build();
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));
}
}
for error in &result.errors {
error.fancy_format_stderr();
}
if !args.no_warnings {
for warning in &result.warnings {
warning.fancy_format_stderr(&source_map);
}
}
Ok(result.value)
}
pub struct InterpretResult {
output: String,
success: bool,
}
impl InterpretResult {
pub fn empty_success() -> Self {
Self {
output: String::new(),
success: true,
}
}
pub fn finalize(self) -> bool {
print!("{}", self.output);
self.success
}
pub fn output(&self) -> &str {
&self.output
}
pub fn success(&self) -> bool {
self.success
}
}
/// Interprets the given code snippet, printing out warnings, errors
/// and the result itself. The return value indicates whether
/// evaluation succeeded.
#[instrument(skip_all, fields(indicatif.pb_show=1))]
pub fn interpret(
tvix_store_io: Rc<TvixStoreIO>,
code: &str,
path: Option<PathBuf>,
args: &Args,
explain: bool,
allow_incomplete: AllowIncomplete,
env: Option<&HashMap<SmolStr, Value>>,
) -> Result<InterpretResult, IncompleteInput> {
let mut output = String::new();
let result = evaluate(tvix_store_io, code, path, args, allow_incomplete, env)?;
if let Some(value) = result.as_ref() {
if explain {
writeln!(&mut output, "=> {}", value.explain()).unwrap();
} else if args.raw {
writeln!(&mut output, "{}", value.to_contextful_str().unwrap()).unwrap();
} else {
writeln!(&mut output, "=> {} :: {}", value, value.type_of()).unwrap();
}
}
// inform the caller about any errors
Ok(InterpretResult {
output,
success: result.is_some(),
})
}

View file

@ -1,23 +1,11 @@
mod assignment;
mod repl;
use clap::Parser; use clap::Parser;
use repl::Repl;
use smol_str::SmolStr;
use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use tracing::{instrument, Level, Span}; use tvix_cli::args::Args;
use tracing_indicatif::span_ext::IndicatifSpanExt; use tvix_cli::repl::Repl;
use tvix_build::buildservice; use tvix_cli::{init_io_handle, interpret, AllowIncomplete};
use tvix_eval::builtins::impure_builtins; use tvix_eval::observer::DisassemblingObserver;
use tvix_eval::observer::{DisassemblingObserver, TracingObserver};
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::tvix_store_io::TvixStoreIO;
use tvix_glue::{builtins::add_derivation_builtins, configure_nix_path};
#[cfg(not(target_env = "msvc"))] #[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc; use tikv_jemallocator::Jemalloc;
@ -26,240 +14,6 @@ use tikv_jemallocator::Jemalloc;
#[global_allocator] #[global_allocator]
static GLOBAL: Jemalloc = Jemalloc; static GLOBAL: Jemalloc = Jemalloc;
#[derive(Parser, Clone)]
struct Args {
/// A global log level to use when printing logs.
/// It's also possible to set `RUST_LOG` according to
/// `tracing_subscriber::filter::EnvFilter`, which will always have
/// priority.
#[arg(long, default_value_t=Level::INFO)]
log_level: Level,
/// Path to a script to evaluate
script: Option<PathBuf>,
#[clap(long, short = 'E')]
expr: Option<String>,
/// Dump the raw AST to stdout before interpreting
#[clap(long, env = "TVIX_DISPLAY_AST")]
display_ast: bool,
/// Dump the bytecode to stdout before evaluating
#[clap(long, env = "TVIX_DUMP_BYTECODE")]
dump_bytecode: bool,
/// Trace the runtime of the VM
#[clap(long, env = "TVIX_TRACE_RUNTIME")]
trace_runtime: bool,
/// Capture the time (relative to the start time of evaluation) of all events traced with
/// `--trace-runtime`
#[clap(long, env = "TVIX_TRACE_RUNTIME_TIMING", requires("trace_runtime"))]
trace_runtime_timing: bool,
/// Only compile, but do not execute code. This will make Tvix act
/// sort of like a linter.
#[clap(long)]
compile_only: bool,
/// Don't print warnings.
#[clap(long)]
no_warnings: bool,
/// A colon-separated list of directories to use to resolve `<...>`-style paths
#[clap(long, short = 'I', env = "NIX_PATH")]
nix_search_path: Option<String>,
/// Print "raw" (unquoted) output.
#[clap(long)]
raw: bool,
/// Strictly evaluate values, traversing them and forcing e.g.
/// elements of lists and attribute sets before printing the
/// return value.
#[clap(long)]
strict: bool,
#[arg(long, env, default_value = "memory://")]
blob_service_addr: String,
#[arg(long, env, default_value = "memory://")]
directory_service_addr: String,
#[arg(long, env, default_value = "memory://")]
path_info_service_addr: String,
#[arg(long, env, default_value = "dummy://")]
build_service_addr: String,
}
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({
let blob_service = blob_service.clone();
let directory_service = directory_service.clone();
async move {
buildservice::from_addr(
&args.build_service_addr,
blob_service.clone(),
directory_service.clone(),
)
.await
}
})
.expect("unable to setup buildservice before interpreter setup");
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 and errors and returning the result
fn evaluate(
tvix_store_io: Rc<TvixStoreIO>,
code: &str,
path: Option<PathBuf>,
args: &Args,
allow_incomplete: AllowIncomplete,
env: Option<&HashMap<SmolStr, Value>>,
) -> Result<Option<Value>, IncompleteInput> {
let span = Span::current();
span.pb_start();
span.pb_set_style(&tvix_tracing::PB_SPINNER_STYLE);
span.pb_set_message("Setting up evaluator…");
let mut eval_builder = tvix_eval::Evaluation::builder(Box::new(TvixIO::new(
tvix_store_io.clone() as Rc<dyn EvalIO>,
)) as Box<dyn EvalIO>)
.enable_import()
.with_strict(args.strict)
.add_builtins(impure_builtins())
.env(env);
eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&tvix_store_io));
eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&tvix_store_io));
eval_builder = add_import_builtins(eval_builder, tvix_store_io);
eval_builder = configure_nix_path(eval_builder, &args.nix_search_path);
let source_map = eval_builder.source_map().clone();
let result = {
let mut compiler_observer =
DisassemblingObserver::new(source_map.clone(), std::io::stderr());
if args.dump_bytecode {
eval_builder.set_compiler_observer(Some(&mut compiler_observer));
}
let mut runtime_observer = TracingObserver::new(std::io::stderr());
if args.trace_runtime {
if args.trace_runtime_timing {
runtime_observer.enable_timing()
}
eval_builder.set_runtime_observer(Some(&mut runtime_observer));
}
span.pb_set_message("Evaluating…");
let eval = eval_builder.build();
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));
}
}
for error in &result.errors {
error.fancy_format_stderr();
}
if !args.no_warnings {
for warning in &result.warnings {
warning.fancy_format_stderr(&source_map);
}
}
Ok(result.value)
}
/// Interprets the given code snippet, printing out warnings, errors
/// and the result itself. The return value indicates whether
/// evaluation succeeded.
#[instrument(skip_all, fields(indicatif.pb_show=1))]
fn interpret(
tvix_store_io: Rc<TvixStoreIO>,
code: &str,
path: Option<PathBuf>,
args: &Args,
explain: bool,
allow_incomplete: AllowIncomplete,
env: Option<&HashMap<SmolStr, Value>>,
) -> Result<bool, IncompleteInput> {
let result = evaluate(tvix_store_io, code, path, args, allow_incomplete, env)?;
if let Some(value) = result.as_ref() {
if explain {
println!("=> {}", value.explain());
} else {
println_result(value, args.raw);
}
}
// inform the caller about any errors
Ok(result.is_some())
}
/// Interpret the given code snippet, but only run the Tvix compiler /// Interpret the given code snippet, but only run the Tvix compiler
/// on it and return errors and warnings. /// on it and return errors and warnings.
fn lint(code: &str, path: Option<PathBuf>, args: &Args) -> bool { fn lint(code: &str, path: Option<PathBuf>, args: &Args) -> bool {
@ -323,12 +77,13 @@ fn main() {
None, // TODO(aspen): Pass in --arg/--argstr here None, // TODO(aspen): Pass in --arg/--argstr here
) )
.unwrap() .unwrap()
.finalize()
{ {
std::process::exit(1); std::process::exit(1);
} }
} else { } else {
let mut repl = Repl::new(); let mut repl = Repl::new(io_handle, &args);
repl.run(io_handle, &args) repl.run()
} }
} }
@ -351,17 +106,10 @@ fn run_file(io_handle: Rc<TvixStoreIO>, mut path: PathBuf, args: &Args) {
None, None,
) )
.unwrap() .unwrap()
.finalize()
}; };
if !success { if !success {
std::process::exit(1); std::process::exit(1);
} }
} }
fn println_result(result: &Value, raw: bool) {
if raw {
println!("{}", result.to_contextful_str().unwrap())
} else {
println!("=> {} :: {}", result, result.type_of())
}
}

View file

@ -6,8 +6,10 @@ use smol_str::SmolStr;
use tvix_eval::Value; use tvix_eval::Value;
use tvix_glue::tvix_store_io::TvixStoreIO; use tvix_glue::tvix_store_io::TvixStoreIO;
use crate::evaluate; use crate::{
use crate::{assignment::Assignment, interpret, AllowIncomplete, Args, IncompleteInput}; assignment::Assignment, evaluate, interpret, AllowIncomplete, Args, IncompleteInput,
InterpretResult,
};
fn state_dir() -> Option<PathBuf> { fn state_dir() -> Option<PathBuf> {
let mut path = dirs::data_dir(); let mut path = dirs::data_dir();
@ -18,7 +20,7 @@ fn state_dir() -> Option<PathBuf> {
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReplCommand<'a> { pub(crate) enum ReplCommand<'a> {
Expr(&'a str), Expr(&'a str),
Assign(Assignment<'a>), Assign(Assignment<'a>),
Explain(&'a str), Explain(&'a str),
@ -65,27 +67,47 @@ The following commands are supported:
} }
} }
#[derive(Debug)] pub struct CommandResult {
pub struct Repl { output: String,
continue_: bool,
}
impl CommandResult {
pub fn finalize(self) -> bool {
print!("{}", self.output);
self.continue_
}
pub fn output(&self) -> &str {
&self.output
}
}
pub struct Repl<'a> {
/// In-progress multiline input, when the input so far doesn't parse as a complete expression /// In-progress multiline input, when the input so far doesn't parse as a complete expression
multiline_input: Option<String>, multiline_input: Option<String>,
rl: Editor<()>, rl: Editor<()>,
/// Local variables defined at the top-level in the repl /// Local variables defined at the top-level in the repl
env: HashMap<SmolStr, Value>, env: HashMap<SmolStr, Value>,
io_handle: Rc<TvixStoreIO>,
args: &'a Args,
} }
impl Repl { impl<'a> Repl<'a> {
pub fn new() -> Self { pub fn new(io_handle: Rc<TvixStoreIO>, args: &'a Args) -> Self {
let rl = Editor::<()>::new().expect("should be able to launch rustyline"); let rl = Editor::<()>::new().expect("should be able to launch rustyline");
Self { Self {
multiline_input: None, multiline_input: None,
rl, rl,
env: HashMap::new(), env: HashMap::new(),
io_handle,
args,
} }
} }
pub fn run(&mut self, io_handle: Rc<TvixStoreIO>, args: &Args) { pub fn run(&mut self) {
if args.compile_only { if self.args.compile_only {
eprintln!("warning: `--compile-only` has no effect on REPL usage!"); eprintln!("warning: `--compile-only` has no effect on REPL usage!");
} }
@ -112,83 +134,8 @@ impl Repl {
let readline = self.rl.readline(prompt); let readline = self.rl.readline(prompt);
match readline { match readline {
Ok(line) => { Ok(line) => {
if line.is_empty() { if !self.send(line).finalize() {
continue; break;
}
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,
Some(&self.env),
),
ReplCommand::Assign(Assignment { ident, value }) => {
match evaluate(
Rc::clone(&io_handle),
&value.to_string(), /* FIXME: don't re-parse */
None,
args,
AllowIncomplete::Allow,
Some(&self.env),
) {
Ok(Some(value)) => {
self.env.insert(ident.into(), value);
Ok(true)
}
Ok(None) => Ok(true),
Err(incomplete) => Err(incomplete),
}
}
ReplCommand::Explain(input) => interpret(
Rc::clone(&io_handle),
input,
None,
args,
true,
AllowIncomplete::Allow,
Some(&self.env),
),
ReplCommand::Print(input) => interpret(
Rc::clone(&io_handle),
input,
None,
&Args {
strict: true,
..(args.clone())
},
false,
AllowIncomplete::Allow,
Some(&self.env),
),
};
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(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break,
@ -204,4 +151,103 @@ impl Repl {
self.rl.save_history(&path).unwrap(); self.rl.save_history(&path).unwrap();
} }
} }
/// Send a line of user input to the REPL. Returns a result indicating the output to show to the
/// user, and whether or not to continue
pub fn send(&mut self, line: String) -> CommandResult {
if line.is_empty() {
return CommandResult {
output: String::new(),
continue_: true,
};
}
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 => {
return CommandResult {
output: String::new(),
continue_: true,
};
}
ReplCommand::Help => {
println!("{}", ReplCommand::HELP);
Ok(InterpretResult::empty_success())
}
ReplCommand::Expr(input) => interpret(
Rc::clone(&self.io_handle),
input,
None,
self.args,
false,
AllowIncomplete::Allow,
Some(&self.env),
),
ReplCommand::Assign(Assignment { ident, value }) => {
match evaluate(
Rc::clone(&self.io_handle),
&value.to_string(), /* FIXME: don't re-parse */
None,
self.args,
AllowIncomplete::Allow,
Some(&self.env),
) {
Ok(Some(value)) => {
self.env.insert(ident.into(), value);
Ok(InterpretResult::empty_success())
}
Ok(None) => Ok(InterpretResult::empty_success()),
Err(incomplete) => Err(incomplete),
}
}
ReplCommand::Explain(input) => interpret(
Rc::clone(&self.io_handle),
input,
None,
self.args,
true,
AllowIncomplete::Allow,
Some(&self.env),
),
ReplCommand::Print(input) => interpret(
Rc::clone(&self.io_handle),
input,
None,
&Args {
strict: true,
..(self.args.clone())
},
false,
AllowIncomplete::Allow,
Some(&self.env),
),
};
match res {
Ok(InterpretResult { output, .. }) => {
self.rl.add_history_entry(input);
self.multiline_input = None;
CommandResult {
output,
continue_: true,
}
}
Err(IncompleteInput) => {
if self.multiline_input.is_none() {
self.multiline_input = Some(line);
}
CommandResult {
output: String::new(),
continue_: true,
}
}
}
}
} }

27
tvix/cli/tests/repl.rs Normal file
View file

@ -0,0 +1,27 @@
use std::ffi::OsString;
use clap::Parser;
use expect_test::expect;
use tvix_cli::init_io_handle;
macro_rules! test_repl {
($name:ident() {$($send:expr => $expect:expr;)*}) => {
#[test]
fn $name() {
let tokio_runtime = tokio::runtime::Runtime::new().unwrap();
let args = tvix_cli::Args::parse_from(Vec::<OsString>::new());
let mut repl = tvix_cli::Repl::new(init_io_handle(&tokio_runtime, &args), &args);
$({
let result = repl.send($send.into());
$expect.assert_eq(result.output())
;
})*
}
}
}
test_repl!(simple_expr_eval() {
"1" => expect![[r#"
=> 1 :: int
"#]];
});