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, VmContext},
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    marker::PhantomData,
29    result::Result,
30    str::FromStr,
31};
32
33#[cfg(feature = "repl")]
34use crate::{
35    error::{
36        report::{self, ColorOpt, ErrorFormat},
37        IntoDiagnostics,
38    },
39    term::Term,
40};
41#[cfg(feature = "repl")]
42use ansi_term::{Colour, Style};
43#[cfg(feature = "repl")]
44use rustyline::validate::{ValidationContext, ValidationResult};
45
46generate_counter!(InputNameCounter, usize);
47
48pub mod command;
49pub mod query_print;
50#[cfg(feature = "repl")]
51pub mod rustyline_frontend;
52#[cfg(feature = "repl-wasm")]
53pub mod simple_frontend;
54#[cfg(feature = "repl-wasm")]
55pub mod wasm_frontend;
56
57/// Result of the evaluation of an input.
58#[derive(Debug, Clone)]
59pub enum EvalResult {
60    /// The input has been evaluated to a term.
61    Evaluated(RichTerm),
62    /// The input was a toplevel let, which has been bound in the environment.
63    Bound(LocIdent),
64}
65
66impl From<RichTerm> for EvalResult {
67    fn from(t: RichTerm) -> Self {
68        EvalResult::Evaluated(t)
69    }
70}
71
72/// Interface of the REPL backend.
73pub trait Repl {
74    /// Evaluate an expression, which can be either a standard term or a toplevel let-binding.
75    fn eval(&mut self, exp: &str) -> Result<EvalResult, Error>;
76    /// Fully evaluate an expression, which can be either a standard term or a toplevel let-binding.
77    fn eval_full(&mut self, exp: &str) -> Result<EvalResult, Error>;
78    /// Load the content of a file in the environment. Return the loaded record.
79    fn load(&mut self, path: impl AsRef<OsStr>) -> Result<RichTerm, Error>;
80    /// Typecheck an expression and return its [apparent type][crate::typecheck::ApparentType].
81    fn typecheck(&mut self, exp: &str) -> Result<Type, Error>;
82    /// Query the metadata of an expression.
83    fn query(&mut self, path: String) -> Result<Field, Error>;
84    /// Required for error reporting on the frontend.
85    fn cache_mut(&mut self) -> &mut CacheHub;
86}
87
88/// Standard implementation of the REPL backend.
89pub struct ReplImpl<EC: EvalCache> {
90    /// The virtual machine context.
91    vm_ctxt: VmContext<CacheHub, EC>,
92    /// The current eval environment, including the stdlib and top-level lets.
93    eval_env: eval::Environment,
94    phantom: PhantomData<EC>,
95}
96
97impl<EC: EvalCache> ReplImpl<EC> {
98    /// Create a new empty REPL.
99    pub fn new(trace: impl Write + 'static) -> Self {
100        ReplImpl {
101            eval_env: eval::Environment::new(),
102            vm_ctxt: VmContext::new(CacheHub::new(), trace, NullReporter {}),
103            phantom: PhantomData,
104        }
105    }
106
107    /// Load and process the stdlib, and use it to populate the eval environment as well as the
108    /// typing environment.
109    pub fn load_stdlib(&mut self) -> Result<(), Error> {
110        self.vm_ctxt.import_resolver.prepare_stdlib()?;
111        self.eval_env = self
112            .vm_ctxt
113            .import_resolver
114            .mk_eval_env(&mut self.vm_ctxt.cache);
115        Ok(())
116    }
117
118    fn eval_(&mut self, exp: &str, eval_full: bool) -> Result<EvalResult, Error> {
119        let file_id = self.vm_ctxt.import_resolver.sources.add_string(
120            SourcePath::ReplInput(InputNameCounter::next()),
121            String::from(exp),
122        );
123
124        let id = self.vm_ctxt.import_resolver.prepare_repl(file_id)?.inner();
125        // unwrap(): we've just prepared the term successfully, so it must be in cache
126        let term = self
127            .vm_ctxt
128            .import_resolver
129            .terms
130            .get_owned(file_id)
131            .unwrap();
132
133        if let Some(id) = id {
134            let current_env = self.eval_env.clone();
135            eval::env_add(
136                &mut self.vm_ctxt.cache,
137                &mut self.eval_env,
138                id,
139                term,
140                current_env,
141            );
142
143            Ok(EvalResult::Bound(id))
144        } else {
145            let closure = Closure {
146                body: term,
147                env: self.eval_env.clone(),
148            };
149
150            let mut vm = VirtualMachine::new(&mut self.vm_ctxt);
151
152            let result = if eval_full {
153                vm.eval_full_closure(closure)
154            } else {
155                vm.eval_closure(closure)
156            };
157
158            Ok(result?.body.into())
159        }
160    }
161
162    #[cfg(feature = "repl")]
163    fn report(&mut self, err: impl IntoDiagnostics, color_opt: ColorOpt) {
164        let mut files = self.vm_ctxt.import_resolver.sources.files().clone();
165        report::report(&mut files, err, ErrorFormat::Text, color_opt);
166    }
167}
168
169impl<EC: EvalCache> Repl for ReplImpl<EC> {
170    fn eval(&mut self, exp: &str) -> Result<EvalResult, Error> {
171        self.eval_(exp, false)
172    }
173
174    fn eval_full(&mut self, exp: &str) -> Result<EvalResult, Error> {
175        self.eval_(exp, true)
176    }
177
178    fn load(&mut self, path: impl AsRef<OsStr>) -> Result<RichTerm, Error> {
179        let file_id = self
180            .vm_ctxt
181            .import_resolver
182            .sources
183            .add_file(OsString::from(path.as_ref()), InputFormat::Nickel)
184            .map_err(IOError::from)?;
185
186        self.vm_ctxt.import_resolver.prepare_repl(file_id)?;
187        let term = self
188            .vm_ctxt
189            .import_resolver
190            .terms
191            .get_owned(file_id)
192            .unwrap();
193        let pos = term.pos;
194
195        let Closure {
196            body: term,
197            env: new_env,
198        } = {
199            VirtualMachine::new(&mut self.vm_ctxt).eval_closure(Closure {
200                body: term,
201                env: self.eval_env.clone(),
202            })?
203        };
204
205        self.vm_ctxt
206            .import_resolver
207            .add_repl_bindings(&term)
208            .map_err(|NotARecord| {
209                Error::EvalError(EvalError::Other(
210                    String::from("load: expected a record"),
211                    pos,
212                ))
213            })?;
214
215        eval::env_add_record(
216            &mut self.vm_ctxt.cache,
217            &mut self.eval_env,
218            Closure {
219                body: term.clone(),
220                env: new_env,
221            },
222        )
223        // unwrap(): if the call above succeeded, the term must be a record
224        .unwrap();
225
226        Ok(term)
227    }
228
229    fn typecheck(&mut self, exp: &str) -> Result<Type, Error> {
230        let cache = &mut self.vm_ctxt.import_resolver;
231
232        let file_id = cache.replace_string(SourcePath::ReplTypecheck, String::from(exp));
233        let _ = cache.parse_to_ast(file_id)?;
234
235        cache
236            .typecheck(file_id, TypecheckMode::Walk)
237            .map_err(|cache_err| {
238                cache_err.unwrap_error(
239                    "repl::typecheck(): expected source to be parsed before typechecking",
240                )
241            })?;
242
243        Ok(cache
244            .type_of(file_id)
245            .map_err(|cache_err| {
246                cache_err.unwrap_error(
247                    "repl::typecheck(): expected source to be parsed before retrieving the type",
248                )
249            })?
250            .inner())
251    }
252
253    fn query(&mut self, path: String) -> Result<Field, Error> {
254        let mut query_path = FieldPath::parse(&mut self.vm_ctxt.import_resolver, path)?;
255
256        // remove(): this is safe because there is no such thing as an empty field path, at least
257        // when it comes out of the parser. If `path` is empty, the parser will error out. Hence,
258        // `FieldPath::parse` always returns a non-empty vector.
259        let target = query_path.0.remove(0);
260
261        let file_id = self
262            .vm_ctxt
263            .import_resolver
264            .replace_string(SourcePath::ReplQuery, target.label().into());
265        self.vm_ctxt.import_resolver.prepare_repl(file_id)?;
266
267        let result = {
268            let body = self
269                .vm_ctxt
270                .import_resolver
271                .terms
272                .get_owned(file_id)
273                .unwrap();
274
275            VirtualMachine::new(&mut self.vm_ctxt).query_closure(
276                Closure {
277                    body,
278                    env: self.eval_env.clone(),
279                },
280                &query_path,
281            )?
282        };
283
284        Ok(result)
285    }
286
287    fn cache_mut(&mut self) -> &mut CacheHub {
288        &mut self.vm_ctxt.import_resolver
289    }
290}
291
292/// Error occurring when initializing the REPL.
293pub enum InitError {
294    /// Unable to load, parse or typecheck the stdlib
295    Stdlib,
296    /// An I/O or Linux Syscall (Errno) error happened while initializing the interactive terminal.
297    ReadlineError(String),
298}
299
300pub enum InputStatus {
301    Complete,
302    Partial,
303    Command,
304    Failed(ParseErrors),
305}
306
307/// Validator enabling multiline input.
308///
309/// The behavior is the following:
310/// - always end an input that starts with the command prefix `:`
311/// - otherwise, try to parse the input. If an unexpected end of file error occurs, continue
312///   the input in a new line. Otherwise, accept and end the input.
313//TODO: the validator throws away the result of parsing, or the parse error, when accepting an
314//input, meaning that the work is done a second time by the REPL. Validator's work could be
315//reused. This overhead shouldn't be dramatic for the typical REPL input size, though.
316#[cfg_attr(
317    feature = "repl",
318    derive(
319        rustyline_derive::Completer,
320        rustyline_derive::Helper,
321        rustyline_derive::Hinter
322    )
323)]
324pub struct InputParser {
325    parser: grammar::ExtendedTermParser,
326    /// Currently the parser expect a `FileId` to fill in location information. For this
327    /// validator, this may be a dummy one, since for now location information is not used.
328    file_id: FileId,
329    /// The allocator used to allocate AST nodes.
330    ///
331    /// Note that we just grow this allocator without ever freeing it. This should be fine for a
332    /// REPL. If that is a problem, it's not very hard to periodically clear it.
333    alloc: AstAlloc,
334}
335
336impl InputParser {
337    pub fn new(file_id: FileId) -> Self {
338        InputParser {
339            parser: grammar::ExtendedTermParser::new(),
340            file_id,
341            alloc: AstAlloc::new(),
342        }
343    }
344
345    pub fn parse(&self, input: &str) -> InputStatus {
346        if input.starts_with(':') || input.trim().is_empty() {
347            return InputStatus::Command;
348        }
349
350        let result =
351            self.parser
352                .parse_tolerant(&self.alloc, self.file_id, lexer::Lexer::new(input));
353
354        let partial = |pe| {
355            matches!(
356                pe,
357                &ParseError::UnexpectedEOF(..) | &ParseError::UnmatchedCloseBrace(..)
358            )
359        };
360
361        match result {
362            Ok((_, e)) if e.no_errors() => InputStatus::Complete,
363            Ok((_, e)) if e.errors.iter().all(partial) => InputStatus::Partial,
364            Ok((_, e)) => InputStatus::Failed(e),
365            Err(e) if partial(&e) => InputStatus::Partial,
366            Err(err) => InputStatus::Failed(err.into()),
367        }
368    }
369}
370
371#[cfg(feature = "repl")]
372impl rustyline::highlight::Highlighter for InputParser {
373    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
374        &'s self,
375        prompt: &'p str,
376        _default: bool,
377    ) -> std::borrow::Cow<'b, str> {
378        let style = Style::new().fg(Colour::Green);
379        std::borrow::Cow::Owned(style.paint(prompt).to_string())
380    }
381}
382
383#[cfg(feature = "repl")]
384impl rustyline::validate::Validator for InputParser {
385    fn validate(&self, ctx: &mut ValidationContext<'_>) -> rustyline::Result<ValidationResult> {
386        match self.parse(ctx.input()) {
387            InputStatus::Partial => Ok(ValidationResult::Invalid(None)),
388            _ => Ok(ValidationResult::Valid(None)),
389        }
390    }
391}
392
393/// Print the help message corresponding to a command, or show a list of available commands if
394/// the argument is `None` or is not a command.
395#[cfg(any(feature = "repl", feature = "repl-wasm"))]
396pub fn print_help(out: &mut impl Write, arg: Option<&str>) -> std::io::Result<()> {
397    use command::*;
398
399    if let Some(arg) = arg {
400        fn print_aliases(w: &mut impl Write, cmd: CommandType) -> std::io::Result<()> {
401            let mut aliases = cmd.aliases().into_iter();
402
403            if let Some(fst) = aliases.next() {
404                write!(w, "Aliases: `{fst}`")?;
405                aliases.try_for_each(|alias| write!(w, ", `{alias}`"))?;
406                writeln!(w)?;
407            }
408
409            writeln!(w)
410        }
411
412        match arg.parse::<CommandType>() {
413            Ok(c @ CommandType::Help) => {
414                writeln!(out, ":{c} [command]")?;
415                print_aliases(out, c)?;
416                writeln!(
417                    out,
418                    "Prints a list of available commands or the help of the given command"
419                )?;
420            }
421            Ok(c @ CommandType::Query) => {
422                writeln!(out, ":{c} <field path>")?;
423                print_aliases(out, c)?;
424                writeln!(out, "Print the metadata attached to a field")?;
425                writeln!(
426                    out,
427                    "<field path> is a dot-separated sequence of identifiers pointing to a field. \
428                    Fields can be quoted if they contain special characters, \
429                    just like in normal Nickel source code.\n"
430                )?;
431                writeln!(out, "Examples:")?;
432                writeln!(out, "- `:{c} std.array.any`")?;
433                writeln!(out, "- `:{c} mylib.contracts.\"special#chars.\".bar`")?;
434            }
435            Ok(c @ CommandType::Load) => {
436                writeln!(out, ":{c} <file>")?;
437                print_aliases(out, c)?;
438                writeln!(
439                    out,
440                    "Evaluate the content of <file> to a record \
441                    and add its fields to the environment."
442                )?;
443                writeln!(
444                    out,
445                    "Fail if the content of <file> doesn't evaluate to a record."
446                )?;
447            }
448            Ok(c @ CommandType::Typecheck) => {
449                writeln!(out, ":{c} <expression>")?;
450                print_aliases(out, c)?;
451                writeln!(
452                    out,
453                    "Typecheck the given expression and print its top-level type"
454                )?;
455            }
456            Ok(c @ CommandType::Print) => {
457                writeln!(out, ":{c} <expression>")?;
458                print_aliases(out, c)?;
459                writeln!(out, "Evaluate and print <expression> recursively")?;
460            }
461            Ok(c @ CommandType::Exit) => {
462                writeln!(out, ":{c}")?;
463                print_aliases(out, c)?;
464                writeln!(out, "Exit the REPL session")?;
465            }
466            Err(UnknownCommandError {}) => {
467                writeln!(out, "Unknown command `{arg}`.")?;
468                writeln!(
469                    out,
470                    "Available commands: ? {}",
471                    CommandType::all().join(" ")
472                )?;
473            }
474        };
475
476        Ok(())
477    } else {
478        writeln!(out, "Available commands: help query load typecheck exit")
479    }
480}