nickel_lang_core/repl/
mod.rs

1//! The Nickel REPL.
2//!
3//! A backend designates a module which actually executes a sequence of REPL commands, while being
4//! agnostic to the user interface and the presentation of the results.
5//!
6//! Dually, the frontend is the user-facing part, which may be a CLI, a web application, a
7//! jupyter-kernel (which is not exactly user-facing, but still manages input/output and
8//! formatting), etc.
9use 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/// Result of the evaluation of an input.
57#[derive(Debug, Clone)]
58pub enum EvalResult {
59    /// The input has been evaluated to a term.
60    Evaluated(RichTerm),
61    /// The input was a toplevel let, which has been bound in the environment.
62    Bound(LocIdent),
63}
64
65impl From<RichTerm> for EvalResult {
66    fn from(t: RichTerm) -> Self {
67        EvalResult::Evaluated(t)
68    }
69}
70
71/// Interface of the REPL backend.
72pub trait Repl {
73    /// Evaluate an expression, which can be either a standard term or a toplevel let-binding.
74    fn eval(&mut self, exp: &str) -> Result<EvalResult, Error>;
75    /// Fully evaluate an expression, which can be either a standard term or a toplevel let-binding.
76    fn eval_full(&mut self, exp: &str) -> Result<EvalResult, Error>;
77    /// Load the content of a file in the environment. Return the loaded record.
78    fn load(&mut self, path: impl AsRef<OsStr>) -> Result<RichTerm, Error>;
79    /// Typecheck an expression and return its [apparent type][crate::typecheck::ApparentType].
80    fn typecheck(&mut self, exp: &str) -> Result<Type, Error>;
81    /// Query the metadata of an expression.
82    fn query(&mut self, path: String) -> Result<Field, Error>;
83    /// Required for error reporting on the frontend.
84    fn cache_mut(&mut self) -> &mut CacheHub;
85}
86
87/// Standard implementation of the REPL backend.
88pub struct ReplImpl<EC: EvalCache> {
89    /// The current eval environment, including the stdlib and top-level lets.
90    eval_env: eval::Environment,
91    /// The state of the Nickel virtual machine, holding a cache of loaded files and parsed terms.
92    vm: VirtualMachine<CacheHub, EC>,
93}
94
95impl<EC: EvalCache> ReplImpl<EC> {
96    /// Create a new empty REPL.
97    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    /// Load and process the stdlib, and use it to populate the eval environment as well as the
105    /// typing environment.
106    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        // unwrap(): we've just prepared the term successfully, so it must be in cache
128        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(): if the call above succeeded, the term must be a record
208        .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        // remove(): this is safe because there is no such thing as an empty field path, at least
243        // when it comes out of the parser. If `path` is empty, the parser will error out. Hence,
244        // `FieldPath::parse` always returns a non-empty vector.
245        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
268/// Error occurring when initializing the REPL.
269pub enum InitError {
270    /// Unable to load, parse or typecheck the stdlib
271    Stdlib,
272    /// An I/O or Linux Syscall (Errno) error happened while initializing the interactive terminal.
273    ReadlineError(String),
274}
275
276pub enum InputStatus {
277    Complete,
278    Partial,
279    Command,
280    Failed(ParseErrors),
281}
282
283/// Validator enabling multiline input.
284///
285/// The behavior is the following:
286/// - always end an input that starts with the command prefix `:`
287/// - otherwise, try to parse the input. If an unexpected end of file error occurs, continue
288///   the input in a new line. Otherwise, accept and end the input.
289//TODO: the validator throws away the result of parsing, or the parse error, when accepting an
290//input, meaning that the work is done a second time by the REPL. Validator's work could be
291//reused. This overhead shouldn't be dramatic for the typical REPL input size, though.
292#[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    /// Currently the parser expect a `FileId` to fill in location information. For this
303    /// validator, this may be a dummy one, since for now location information is not used.
304    file_id: FileId,
305    /// The allocator used to allocate AST nodes.
306    ///
307    /// Note that we just grow this allocator without ever freeing it. This should be fine for a
308    /// REPL. If that is a problem, it's not very hard to periodically clear it.
309    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/// Print the help message corresponding to a command, or show a list of available commands if
370/// the argument is `None` or is not a command.
371#[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}