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