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, 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#[derive(Debug, Clone)]
59pub enum EvalResult {
60 Evaluated(RichTerm),
62 Bound(LocIdent),
64}
65
66impl From<RichTerm> for EvalResult {
67 fn from(t: RichTerm) -> Self {
68 EvalResult::Evaluated(t)
69 }
70}
71
72pub trait Repl {
74 fn eval(&mut self, exp: &str) -> Result<EvalResult, Error>;
76 fn eval_full(&mut self, exp: &str) -> Result<EvalResult, Error>;
78 fn load(&mut self, path: impl AsRef<OsStr>) -> Result<RichTerm, Error>;
80 fn typecheck(&mut self, exp: &str) -> Result<Type, Error>;
82 fn query(&mut self, path: String) -> Result<Field, Error>;
84 fn cache_mut(&mut self) -> &mut CacheHub;
86}
87
88pub struct ReplImpl<EC: EvalCache> {
90 vm_ctxt: VmContext<CacheHub, EC>,
92 eval_env: eval::Environment,
94 phantom: PhantomData<EC>,
95}
96
97impl<EC: EvalCache> ReplImpl<EC> {
98 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 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 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();
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 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
292pub enum InitError {
294 Stdlib,
296 ReadlineError(String),
298}
299
300pub enum InputStatus {
301 Complete,
302 Partial,
303 Command,
304 Failed(ParseErrors),
305}
306
307#[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 file_id: FileId,
329 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#[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}