From fc63594631590547c9a31001806095f2e079a20e Mon Sep 17 00:00:00 2001 From: Aspen Smith Date: Thu, 4 Jul 2024 23:46:20 -0400 Subject: [PATCH] feat(tvix/repl): Allow binding variables at the top-level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow binding variables at the REPL's toplevel in the same way the Nix REPL does, using the syntax = . This fully, strictly evaluates the value and sets it in the repl's "env", which gets passed in at the toplevel when evaluating expressions. The laziness behavior differs from Nix's, but I think this is good: ❯ nix repl Welcome to Nix version 2.3.18. Type :? for help. nix-repl> x = builtins.trace "x" 1 nix-repl> x trace: x 1 nix-repl> x 1 vs tvix: tvix-repl> x = builtins.trace "x" 1 trace: "x" :: string tvix-repl> x => 1 :: int tvix-repl> x => 1 :: int Bug: https://b.tvl.fyi/issues/371 Change-Id: Ieb2d626b7195fa87be638c9a4dae2eee45eb9ab1 Reviewed-on: https://cl.tvl.fyi/c/depot/+/11954 Reviewed-by: flokli Tested-by: BuildkiteCI Autosubmit: aspen --- tvix/Cargo.lock | 2 ++ tvix/Cargo.nix | 8 +++++ tvix/cli/Cargo.toml | 2 ++ tvix/cli/src/assignment.rs | 74 ++++++++++++++++++++++++++++++++++++++ tvix/cli/src/main.rs | 41 ++++++++++++++++----- tvix/cli/src/repl.rs | 46 ++++++++++++++++++++---- 6 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 tvix/cli/src/assignment.rs diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock index 731731e72..bbdeaeefb 100644 --- a/tvix/Cargo.lock +++ b/tvix/Cargo.lock @@ -4367,7 +4367,9 @@ dependencies = [ "dirs", "nix-compat", "rnix", + "rowan", "rustyline", + "smol_str", "thiserror", "tikv-jemallocator", "tokio", diff --git a/tvix/Cargo.nix b/tvix/Cargo.nix index 4d6e1d946..bf0958407 100644 --- a/tvix/Cargo.nix +++ b/tvix/Cargo.nix @@ -13845,10 +13845,18 @@ rec { name = "rnix"; packageId = "rnix"; } + { + name = "rowan"; + packageId = "rowan"; + } { name = "rustyline"; packageId = "rustyline"; } + { + name = "smol_str"; + packageId = "smol_str"; + } { name = "thiserror"; packageId = "thiserror"; diff --git a/tvix/cli/Cargo.toml b/tvix/cli/Cargo.toml index 644393a7c..f6d03ce5a 100644 --- a/tvix/cli/Cargo.toml +++ b/tvix/cli/Cargo.toml @@ -20,6 +20,8 @@ clap = { version = "4.0", features = ["derive", "env"] } dirs = "4.0.0" rustyline = "10.0.0" rnix = "0.11.0" +rowan = "*" +smol_str = "0.2.0" thiserror = "1.0.38" tokio = "1.28.0" tracing = "0.1.40" diff --git a/tvix/cli/src/assignment.rs b/tvix/cli/src/assignment.rs new file mode 100644 index 000000000..6fd9725d2 --- /dev/null +++ b/tvix/cli/src/assignment.rs @@ -0,0 +1,74 @@ +use rnix::{Root, SyntaxKind, SyntaxNode}; +use rowan::ast::AstNode; + +/// An assignment of an identifier to a value in the context of a REPL. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Assignment<'a> { + pub(crate) ident: &'a str, + pub(crate) value: rnix::ast::Expr, +} + +impl<'a> Assignment<'a> { + /// Try to parse an [`Assignment`] from the given input string. + /// + /// Returns [`None`] if the parsing fails for any reason, since the intent is for us to + /// fall-back to trying to parse the input as a regular expression or other REPL commands for + /// any reason, since the intent is for us to fall-back to trying to parse the input as a + /// regular expression or other REPL command. + pub fn parse(input: &'a str) -> Option { + let mut tt = rnix::tokenizer::Tokenizer::new(input); + macro_rules! next { + ($kind:ident) => {{ + loop { + let (kind, tok) = tt.next()?; + if kind == SyntaxKind::TOKEN_WHITESPACE { + continue; + } + if kind != SyntaxKind::$kind { + return None; + } + break tok; + } + }}; + } + + let ident = next!(TOKEN_IDENT); + let _equal = next!(TOKEN_ASSIGN); + let (green, errs) = rnix::parser::parse(tt); + let value = Root::cast(SyntaxNode::new_root(green))?.expr()?; + + if !errs.is_empty() { + return None; + } + + Some(Self { ident, value }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn simple_assignments() { + for input in ["x = 4", "x = \t\t\n\t4", "x=4"] { + let res = Assignment::parse(input).unwrap(); + assert_eq!(res.ident, "x"); + assert_eq!(res.value.to_string(), "4"); + } + } + + #[test] + fn complex_exprs() { + let input = "x = { y = 4; z = let q = 7; in [ q (y // { z = 9; }) ]; }"; + let res = Assignment::parse(input).unwrap(); + assert_eq!(res.ident, "x"); + } + + #[test] + fn not_an_assignment() { + let input = "{ x = 4; }"; + let res = Assignment::parse(input); + assert!(res.is_none(), "{input:?}"); + } +} diff --git a/tvix/cli/src/main.rs b/tvix/cli/src/main.rs index 686513b77..c4c70d2a1 100644 --- a/tvix/cli/src/main.rs +++ b/tvix/cli/src/main.rs @@ -1,7 +1,10 @@ +mod assignment; mod repl; use clap::Parser; use repl::Repl; +use smol_str::SmolStr; +use std::collections::HashMap; use std::rc::Rc; use std::{fs, path::PathBuf}; use tracing::{instrument, Level, Span}; @@ -150,18 +153,15 @@ impl AllowIncomplete { #[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. -#[instrument(skip_all, fields(indicatif.pb_show=1))] -fn interpret( +/// Interprets the given code snippet, printing out warnings and errors and returning the result +fn evaluate( tvix_store_io: Rc, code: &str, path: Option, args: &Args, - explain: bool, allow_incomplete: AllowIncomplete, -) -> Result { + env: Option<&HashMap>, +) -> Result, IncompleteInput> { let span = Span::current(); span.pb_start(); span.pb_set_style(&tvix_tracing::PB_SPINNER_STYLE); @@ -173,6 +173,9 @@ fn interpret( ); eval.strict = args.strict; eval.builtins.extend(impure_builtins()); + if let Some(env) = env { + eval.env = Some(env); + } add_derivation_builtins(&mut eval, Rc::clone(&tvix_store_io)); add_fetcher_builtins(&mut eval, Rc::clone(&tvix_store_io)); add_import_builtins(&mut eval, tvix_store_io); @@ -226,7 +229,25 @@ fn interpret( } } - if let Some(value) = result.value.as_ref() { + 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, + code: &str, + path: Option, + args: &Args, + explain: bool, + allow_incomplete: AllowIncomplete, + env: Option<&HashMap>, +) -> Result { + 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 { @@ -235,7 +256,7 @@ fn interpret( } // inform the caller about any errors - Ok(result.errors.is_empty()) + Ok(result.is_some()) } /// Interpret the given code snippet, but only run the Tvix compiler @@ -298,6 +319,7 @@ fn main() { &args, false, AllowIncomplete::RequireComplete, + None, // TODO(aspen): Pass in --arg/--argstr here ) .unwrap() { @@ -325,6 +347,7 @@ fn run_file(io_handle: Rc, mut path: PathBuf, args: &Args) { args, false, AllowIncomplete::RequireComplete, + None, ) .unwrap() }; diff --git a/tvix/cli/src/repl.rs b/tvix/cli/src/repl.rs index 5a4830a02..758874016 100644 --- a/tvix/cli/src/repl.rs +++ b/tvix/cli/src/repl.rs @@ -1,10 +1,13 @@ -use std::path::PathBuf; use std::rc::Rc; +use std::{collections::HashMap, path::PathBuf}; use rustyline::{error::ReadlineError, Editor}; +use smol_str::SmolStr; +use tvix_eval::Value; use tvix_glue::tvix_store_io::TvixStoreIO; -use crate::{interpret, AllowIncomplete, Args, IncompleteInput}; +use crate::evaluate; +use crate::{assignment::Assignment, interpret, AllowIncomplete, Args, IncompleteInput}; fn state_dir() -> Option { let mut path = dirs::data_dir(); @@ -17,6 +20,7 @@ fn state_dir() -> Option { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ReplCommand<'a> { Expr(&'a str), + Assign(Assignment<'a>), Explain(&'a str), Print(&'a str), Quit, @@ -29,11 +33,12 @@ Welcome to the Tvix REPL! The following commands are supported: - Evaluate a Nix language expression and print the result, along with its inferred type - :d Evaluate a Nix language expression and print a detailed description of the result - :p Evaluate a Nix language expression and print the result recursively - :q Exit the REPL - :?, :h Display this help text + Evaluate a Nix language expression and print the result, along with its inferred type + = Bind the result of an expression to a variable + :d Evaluate a Nix language expression and print a detailed description of the result + :p 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 { @@ -52,6 +57,10 @@ The following commands are supported: } } + if let Some(assignment) = Assignment::parse(input) { + return Self::Assign(assignment); + } + Self::Expr(input) } } @@ -61,6 +70,8 @@ pub struct Repl { /// In-progress multiline input, when the input so far doesn't parse as a complete expression multiline_input: Option, rl: Editor<()>, + /// Local variables defined at the top-level in the repl + env: HashMap, } impl Repl { @@ -69,6 +80,7 @@ impl Repl { Self { multiline_input: None, rl, + env: HashMap::new(), } } @@ -125,7 +137,25 @@ impl Repl { 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, @@ -133,6 +163,7 @@ impl Repl { args, true, AllowIncomplete::Allow, + Some(&self.env), ), ReplCommand::Print(input) => interpret( Rc::clone(&io_handle), @@ -144,6 +175,7 @@ impl Repl { }, false, AllowIncomplete::Allow, + Some(&self.env), ), };