1use crate::{
10 bytecode::ast::AstAlloc,
11 cache::{CacheHub, InputFormat, NotARecord, SourcePath},
12 error::{Error, EvalError, IOError, NullReporter, ParseError, ParseErrors, ReplError},
13 eval::{self, cache::Cache as EvalCache, Closure, VirtualMachine},
14 files::FileId,
15 identifier::LocIdent,
16 parser::{grammar, lexer, ErrorTolerantParser},
17 program::FieldPath,
18 term::{record::Field, RichTerm},
19 typ::Type,
20 typecheck::TypecheckMode,
21};
22
23use simple_counter::*;
24
25use std::{
26 ffi::{OsStr, OsString},
27 io::Write,
28 result::Result,
29 str::FromStr,
30};
31
32#[cfg(feature = "repl")]
33use crate::{
34 error::{
35 report::{self, ColorOpt, ErrorFormat},
36 IntoDiagnostics,
37 },
38 term::Term,
39};
40#[cfg(feature = "repl")]
41use ansi_term::{Colour, Style};
42#[cfg(feature = "repl")]
43use rustyline::validate::{ValidationContext, ValidationResult};
44
45generate_counter!(InputNameCounter, usize);
46
47pub mod command;
48pub mod query_print;
49#[cfg(feature = "repl")]
50pub mod rustyline_frontend;
51#[cfg(feature = "repl-wasm")]
52pub mod simple_frontend;
53#[cfg(feature = "repl-wasm")]
54pub mod wasm_frontend;
55
56#[derive(Debug, Clone)]
58pub enum EvalResult {
59 Evaluated(RichTerm),
61 Bound(LocIdent),
63}
64
65impl From<RichTerm> for EvalResult {
66 fn from(t: RichTerm) -> Self {
67 EvalResult::Evaluated(t)
68 }
69}
70
71pub trait Repl {
73 fn eval(&mut self, exp: &str) -> Result<EvalResult, Error>;
75 fn eval_full(&mut self, exp: &str) -> Result<EvalResult, Error>;
77 fn load(&mut self, path: impl AsRef<OsStr>) -> Result<RichTerm, Error>;
79 fn typecheck(&mut self, exp: &str) -> Result<Type, Error>;
81 fn query(&mut self, path: String) -> Result<Field, Error>;
83 fn cache_mut(&mut self) -> &mut CacheHub;
85}
86
87pub struct ReplImpl<EC: EvalCache> {
89 eval_env: eval::Environment,
91 vm: VirtualMachine<CacheHub, EC>,
93}
94
95impl<EC: EvalCache> ReplImpl<EC> {
96 pub fn new(trace: impl Write + 'static) -> Self {
98 ReplImpl {
99 eval_env: eval::Environment::new(),
100 vm: VirtualMachine::new(CacheHub::new(), trace, NullReporter {}),
101 }
102 }
103
104 pub fn load_stdlib(&mut self) -> Result<(), Error> {
107 self.vm.prepare_stdlib()?;
108 self.eval_env = self.vm.mk_eval_env();
109 Ok(())
110 }
111
112 fn eval_(&mut self, exp: &str, eval_full: bool) -> Result<EvalResult, Error> {
113 self.vm.reset();
114
115 let eval_function = if eval_full {
116 eval::VirtualMachine::eval_full_closure
117 } else {
118 eval::VirtualMachine::eval_closure
119 };
120
121 let file_id = self.vm.import_resolver_mut().sources.add_string(
122 SourcePath::ReplInput(InputNameCounter::next()),
123 String::from(exp),
124 );
125
126 let id = self.vm.import_resolver_mut().prepare_repl(file_id)?.inner();
127 let term = self.vm.import_resolver().terms.get_owned(file_id).unwrap();
129
130 if let Some(id) = id {
131 let current_env = self.eval_env.clone();
132 eval::env_add(
133 &mut self.vm.cache,
134 &mut self.eval_env,
135 id,
136 term,
137 current_env,
138 );
139 Ok(EvalResult::Bound(id))
140 } else {
141 Ok(eval_function(
142 &mut self.vm,
143 Closure {
144 body: term,
145 env: self.eval_env.clone(),
146 },
147 )?
148 .body
149 .into())
150 }
151 }
152
153 #[cfg(feature = "repl")]
154 fn report(&mut self, err: impl IntoDiagnostics, color_opt: ColorOpt) {
155 let mut files = self.cache_mut().sources.files().clone();
156 report::report(&mut files, err, ErrorFormat::Text, color_opt);
157 }
158}
159
160impl<EC: EvalCache> Repl for ReplImpl<EC> {
161 fn eval(&mut self, exp: &str) -> Result<EvalResult, Error> {
162 self.eval_(exp, false)
163 }
164
165 fn eval_full(&mut self, exp: &str) -> Result<EvalResult, Error> {
166 self.eval_(exp, true)
167 }
168
169 fn load(&mut self, path: impl AsRef<OsStr>) -> Result<RichTerm, Error> {
170 let file_id = self
171 .vm
172 .import_resolver_mut()
173 .sources
174 .add_file(OsString::from(path.as_ref()), InputFormat::Nickel)
175 .map_err(IOError::from)?;
176
177 self.vm.import_resolver_mut().prepare_repl(file_id)?;
178 let term = self.vm.import_resolver().terms.get_owned(file_id).unwrap();
179 let pos = term.pos;
180
181 let Closure {
182 body: term,
183 env: new_env,
184 } = self.vm.eval_closure(Closure {
185 body: term,
186 env: self.eval_env.clone(),
187 })?;
188
189 self.vm
190 .import_resolver_mut()
191 .add_repl_bindings(&term)
192 .map_err(|NotARecord| {
193 Error::EvalError(EvalError::Other(
194 String::from("load: expected a record"),
195 pos,
196 ))
197 })?;
198
199 eval::env_add_record(
200 &mut self.vm.cache,
201 &mut self.eval_env,
202 Closure {
203 body: term.clone(),
204 env: new_env,
205 },
206 )
207 .unwrap();
209
210 Ok(term)
211 }
212
213 fn typecheck(&mut self, exp: &str) -> Result<Type, Error> {
214 let cache = self.vm.import_resolver_mut();
215
216 let file_id = cache.replace_string(SourcePath::ReplTypecheck, String::from(exp));
217 let _ = cache.parse_to_ast(file_id)?;
218
219 cache
220 .typecheck(file_id, TypecheckMode::Walk)
221 .map_err(|cache_err| {
222 cache_err.unwrap_error(
223 "repl::typecheck(): expected source to be parsed before typechecking",
224 )
225 })?;
226
227 Ok(cache
228 .type_of(file_id)
229 .map_err(|cache_err| {
230 cache_err.unwrap_error(
231 "repl::typecheck(): expected source to be parsed before retrieving the type",
232 )
233 })?
234 .inner())
235 }
236
237 fn query(&mut self, path: String) -> Result<Field, Error> {
238 self.vm.reset();
239
240 let mut query_path = FieldPath::parse(self.vm.import_resolver_mut(), path)?;
241
242 let target = query_path.0.remove(0);
246
247 let file_id = self
248 .vm
249 .import_resolver_mut()
250 .replace_string(SourcePath::ReplQuery, target.label().into());
251
252 self.vm.import_resolver_mut().prepare_repl(file_id)?;
253
254 Ok(self.vm.query_closure(
255 Closure {
256 body: self.vm.import_resolver().terms.get_owned(file_id).unwrap(),
257 env: self.eval_env.clone(),
258 },
259 &query_path,
260 )?)
261 }
262
263 fn cache_mut(&mut self) -> &mut CacheHub {
264 self.vm.import_resolver_mut()
265 }
266}
267
268pub enum InitError {
270 Stdlib,
272 ReadlineError(String),
274}
275
276pub enum InputStatus {
277 Complete,
278 Partial,
279 Command,
280 Failed(ParseErrors),
281}
282
283#[cfg_attr(
293 feature = "repl",
294 derive(
295 rustyline_derive::Completer,
296 rustyline_derive::Helper,
297 rustyline_derive::Hinter
298 )
299)]
300pub struct InputParser {
301 parser: grammar::ExtendedTermParser,
302 file_id: FileId,
305 alloc: AstAlloc,
310}
311
312impl InputParser {
313 pub fn new(file_id: FileId) -> Self {
314 InputParser {
315 parser: grammar::ExtendedTermParser::new(),
316 file_id,
317 alloc: AstAlloc::new(),
318 }
319 }
320
321 pub fn parse(&self, input: &str) -> InputStatus {
322 if input.starts_with(':') || input.trim().is_empty() {
323 return InputStatus::Command;
324 }
325
326 let result =
327 self.parser
328 .parse_tolerant(&self.alloc, self.file_id, lexer::Lexer::new(input));
329
330 let partial = |pe| {
331 matches!(
332 pe,
333 &ParseError::UnexpectedEOF(..) | &ParseError::UnmatchedCloseBrace(..)
334 )
335 };
336
337 match result {
338 Ok((_, e)) if e.no_errors() => InputStatus::Complete,
339 Ok((_, e)) if e.errors.iter().all(partial) => InputStatus::Partial,
340 Ok((_, e)) => InputStatus::Failed(e),
341 Err(e) if partial(&e) => InputStatus::Partial,
342 Err(err) => InputStatus::Failed(err.into()),
343 }
344 }
345}
346
347#[cfg(feature = "repl")]
348impl rustyline::highlight::Highlighter for InputParser {
349 fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
350 &'s self,
351 prompt: &'p str,
352 _default: bool,
353 ) -> std::borrow::Cow<'b, str> {
354 let style = Style::new().fg(Colour::Green);
355 std::borrow::Cow::Owned(style.paint(prompt).to_string())
356 }
357}
358
359#[cfg(feature = "repl")]
360impl rustyline::validate::Validator for InputParser {
361 fn validate(&self, ctx: &mut ValidationContext<'_>) -> rustyline::Result<ValidationResult> {
362 match self.parse(ctx.input()) {
363 InputStatus::Partial => Ok(ValidationResult::Invalid(None)),
364 _ => Ok(ValidationResult::Valid(None)),
365 }
366 }
367}
368
369#[cfg(any(feature = "repl", feature = "repl-wasm"))]
372pub fn print_help(out: &mut impl Write, arg: Option<&str>) -> std::io::Result<()> {
373 use command::*;
374
375 if let Some(arg) = arg {
376 fn print_aliases(w: &mut impl Write, cmd: CommandType) -> std::io::Result<()> {
377 let mut aliases = cmd.aliases().into_iter();
378
379 if let Some(fst) = aliases.next() {
380 write!(w, "Aliases: `{fst}`")?;
381 aliases.try_for_each(|alias| write!(w, ", `{alias}`"))?;
382 writeln!(w)?;
383 }
384
385 writeln!(w)
386 }
387
388 match arg.parse::<CommandType>() {
389 Ok(c @ CommandType::Help) => {
390 writeln!(out, ":{c} [command]")?;
391 print_aliases(out, c)?;
392 writeln!(
393 out,
394 "Prints a list of available commands or the help of the given command"
395 )?;
396 }
397 Ok(c @ CommandType::Query) => {
398 writeln!(out, ":{c} <field path>")?;
399 print_aliases(out, c)?;
400 writeln!(out, "Print the metadata attached to a field")?;
401 writeln!(
402 out,
403 "<field path> is a dot-separated sequence of identifiers pointing to a field. \
404 Fields can be quoted if they contain special characters, \
405 just like in normal Nickel source code.\n"
406 )?;
407 writeln!(out, "Examples:")?;
408 writeln!(out, "- `:{c} std.array.any`")?;
409 writeln!(out, "- `:{c} mylib.contracts.\"special#chars.\".bar`")?;
410 }
411 Ok(c @ CommandType::Load) => {
412 writeln!(out, ":{c} <file>")?;
413 print_aliases(out, c)?;
414 writeln!(
415 out,
416 "Evaluate the content of <file> to a record \
417 and add its fields to the environment."
418 )?;
419 writeln!(
420 out,
421 "Fail if the content of <file> doesn't evaluate to a record."
422 )?;
423 }
424 Ok(c @ CommandType::Typecheck) => {
425 writeln!(out, ":{c} <expression>")?;
426 print_aliases(out, c)?;
427 writeln!(
428 out,
429 "Typecheck the given expression and print its top-level type"
430 )?;
431 }
432 Ok(c @ CommandType::Print) => {
433 writeln!(out, ":{c} <expression>")?;
434 print_aliases(out, c)?;
435 writeln!(out, "Evaluate and print <expression> recursively")?;
436 }
437 Ok(c @ CommandType::Exit) => {
438 writeln!(out, ":{c}")?;
439 print_aliases(out, c)?;
440 writeln!(out, "Exit the REPL session")?;
441 }
442 Err(UnknownCommandError {}) => {
443 writeln!(out, "Unknown command `{arg}`.")?;
444 writeln!(
445 out,
446 "Available commands: ? {}",
447 CommandType::all().join(" ")
448 )?;
449 }
450 };
451
452 Ok(())
453 } else {
454 writeln!(out, "Available commands: help query load typecheck exit")
455 }
456}