snix_cli/
repl.rs

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