tvix_cli/
repl.rs

1use std::path::PathBuf;
2use std::rc::Rc;
3
4use rustc_hash::FxHashMap;
5use rustyline::{error::ReadlineError, Editor};
6use smol_str::SmolStr;
7use tvix_eval::{GlobalsMap, SourceCode, Value};
8use tvix_glue::tvix_store_io::TvixStoreIO;
9
10use crate::{
11    assignment::Assignment, evaluate, interpret, AllowIncomplete, Args, IncompleteInput,
12    InterpretResult,
13};
14
15fn state_dir() -> Option<PathBuf> {
16    let mut path = dirs::data_dir();
17    if let Some(p) = path.as_mut() {
18        p.push("tvix")
19    }
20    path
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub(crate) enum ReplCommand<'a> {
25    Expr(&'a str),
26    Assign(Assignment<'a>),
27    Explain(&'a str),
28    Print(&'a str),
29    Quit,
30    Help,
31}
32
33impl<'a> ReplCommand<'a> {
34    const HELP: &'static str = "
35Welcome to the Tvix REPL!
36
37The following commands are supported:
38
39  <expr>       Evaluate a Nix language expression and print the result, along with its inferred type
40  <x> = <expr> Bind the result of an expression to a variable
41  :d <expr>    Evaluate a Nix language expression and print a detailed description of the result
42  :p <expr>    Evaluate a Nix language expression and print the result recursively
43  :q           Exit the REPL
44  :?, :h       Display this help text
45";
46
47    pub fn parse(input: &'a str) -> Self {
48        if input.starts_with(':') {
49            if let Some(without_prefix) = input.strip_prefix(":d ") {
50                return Self::Explain(without_prefix);
51            } else if let Some(without_prefix) = input.strip_prefix(":p ") {
52                return Self::Print(without_prefix);
53            }
54
55            let input = input.trim_end();
56            match input {
57                ":q" => return Self::Quit,
58                ":h" | ":?" => return Self::Help,
59                _ => {}
60            }
61        }
62
63        if let Some(assignment) = Assignment::parse(input) {
64            return Self::Assign(assignment);
65        }
66
67        Self::Expr(input)
68    }
69}
70
71pub struct CommandResult {
72    output: String,
73    continue_: bool,
74}
75
76impl CommandResult {
77    pub fn finalize(self) -> bool {
78        print!("{}", self.output);
79        self.continue_
80    }
81
82    pub fn output(&self) -> &str {
83        &self.output
84    }
85}
86
87pub struct Repl<'a> {
88    /// In-progress multiline input, when the input so far doesn't parse as a complete expression
89    multiline_input: Option<String>,
90    rl: Editor<()>,
91    /// Local variables defined at the top-level in the repl
92    env: FxHashMap<SmolStr, Value>,
93
94    io_handle: Rc<TvixStoreIO>,
95    args: &'a Args,
96    source_map: SourceCode,
97    globals: Option<Rc<GlobalsMap>>,
98}
99
100impl<'a> Repl<'a> {
101    pub fn new(io_handle: Rc<TvixStoreIO>, args: &'a Args) -> Self {
102        let rl = Editor::<()>::new().expect("should be able to launch rustyline");
103        Self {
104            multiline_input: None,
105            rl,
106            env: FxHashMap::default(),
107            io_handle,
108            args,
109            source_map: Default::default(),
110            globals: None,
111        }
112    }
113
114    pub fn run(&mut self) {
115        if self.args.compile_only {
116            eprintln!("warning: `--compile-only` has no effect on REPL usage!");
117        }
118
119        let history_path = match state_dir() {
120            // Attempt to set up these paths, but do not hard fail if it
121            // doesn't work.
122            Some(mut path) => {
123                let _ = std::fs::create_dir_all(&path);
124                path.push("history.txt");
125                let _ = self.rl.load_history(&path);
126                Some(path)
127            }
128
129            None => None,
130        };
131
132        loop {
133            let prompt = if self.multiline_input.is_some() {
134                "         > "
135            } else {
136                "tvix-repl> "
137            };
138
139            let readline = self.rl.readline(prompt);
140            match readline {
141                Ok(line) => {
142                    if !self.send(line).finalize() {
143                        break;
144                    }
145                }
146                Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break,
147
148                Err(err) => {
149                    eprintln!("error: {err}");
150                    break;
151                }
152            }
153        }
154
155        if let Some(path) = history_path {
156            self.rl.save_history(&path).unwrap();
157        }
158    }
159
160    /// Send a line of user input to the REPL. Returns a result indicating the output to show to the
161    /// user, and whether or not to continue
162    pub fn send(&mut self, line: String) -> CommandResult {
163        if line.is_empty() {
164            return CommandResult {
165                output: String::new(),
166                continue_: true,
167            };
168        }
169
170        let input = if let Some(mi) = &mut self.multiline_input {
171            mi.push('\n');
172            mi.push_str(&line);
173            mi
174        } else {
175            &line
176        };
177
178        let res = match ReplCommand::parse(input) {
179            ReplCommand::Quit => {
180                return CommandResult {
181                    output: String::new(),
182                    continue_: false,
183                };
184            }
185            ReplCommand::Help => {
186                println!("{}", ReplCommand::HELP);
187                Ok(InterpretResult::empty_success(None))
188            }
189            ReplCommand::Expr(input) => interpret(
190                Rc::clone(&self.io_handle),
191                input,
192                None,
193                self.args,
194                false,
195                AllowIncomplete::Allow,
196                Some(&self.env),
197                self.globals.clone(),
198                Some(self.source_map.clone()),
199            ),
200            ReplCommand::Assign(Assignment { ident, value }) => {
201                match evaluate(
202                    Rc::clone(&self.io_handle),
203                    &value.to_string(), /* FIXME: don't re-parse */
204                    None,
205                    self.args,
206                    AllowIncomplete::Allow,
207                    Some(&self.env),
208                    self.globals.clone(),
209                    Some(self.source_map.clone()),
210                ) {
211                    Ok(result) => {
212                        if let Some(value) = result.value {
213                            self.env.insert(ident.into(), value);
214                        }
215                        Ok(InterpretResult::empty_success(Some(result.globals)))
216                    }
217                    Err(incomplete) => Err(incomplete),
218                }
219            }
220            ReplCommand::Explain(input) => interpret(
221                Rc::clone(&self.io_handle),
222                input,
223                None,
224                self.args,
225                true,
226                AllowIncomplete::Allow,
227                Some(&self.env),
228                self.globals.clone(),
229                Some(self.source_map.clone()),
230            ),
231            ReplCommand::Print(input) => interpret(
232                Rc::clone(&self.io_handle),
233                input,
234                None,
235                &Args {
236                    strict: true,
237                    ..(self.args.clone())
238                },
239                false,
240                AllowIncomplete::Allow,
241                Some(&self.env),
242                self.globals.clone(),
243                Some(self.source_map.clone()),
244            ),
245        };
246
247        match res {
248            Ok(InterpretResult {
249                output,
250                globals,
251                success: _,
252            }) => {
253                self.rl.add_history_entry(input);
254                self.multiline_input = None;
255                if globals.is_some() {
256                    self.globals = globals;
257                }
258                CommandResult {
259                    output,
260                    continue_: true,
261                }
262            }
263            Err(IncompleteInput) => {
264                if self.multiline_input.is_none() {
265                    self.multiline_input = Some(line);
266                }
267                CommandResult {
268                    output: String::new(),
269                    continue_: true,
270                }
271            }
272        }
273    }
274}