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 multiline_input: Option<String>,
91 rl: Editor<()>,
92 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 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 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(), 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}