Repository: tweag/nickel
Pull request #456 (2021-11-09 20:23:40)

Tolerate parse errors in terms
This is the beginning of my effort to produce useful information for NLS in the presence of errors.

We now type check and parse with errors.

Avi-D-coder: > This is the way to go, but I left a few comments about the API. In practice though, I have a mixed experience. If I use the following chunk:
> 
> ```
> let y = (.) in
> let x = {foo = 1, bar = 2, baz = 3} in
> (.) + x.bar == 3
> ```
> 
> I do get two different parsing errors, that seems to work. However, I'm still unable to use other LSP functions not involved in the problem like go to definition for `x` or hover on other terms like `3`, and that even if I remove the second parsing error. Do you have an idea about what's happening? @ysndr maybe this is not sufficient to make the linearization work?

I have been trying to narrow lexical scope of the errors, but the grammar keeps failing.

Patch:
diff --git a/lsp/nls/src/cache.rs b/lsp/nls/src/cache.rs
index d6ffe116..d1ba016d 100644
--- a/lsp/nls/src/cache.rs
+++ b/lsp/nls/src/cache.rs
@@ -2,7 +2,7 @@ use std::{collections::HashMap, ffi::OsString, io};
 
 use codespan::FileId;
 use nickel::{
-    cache::{Cache, CacheError, CacheOp, EntryState},
+    cache::{Cache, CacheError, CacheOp, CachedTerm, EntryState},
     error::TypecheckError,
     eval,
     typecheck::{self, linearization::Completed},
@@ -43,13 +43,13 @@ impl CacheExt for Cache {
         }
 
         // After self.parse(), the cache must be populated
-        let (t, state) = self.terms().get(&file_id).unwrap();
+        let CachedTerm { term, state, .. } = self.terms().get(&file_id).unwrap();
 
         if *state > EntryState::Typechecked && lin_cache.contains_key(&file_id) {
             Ok(CacheOp::Cached(()))
         } else if *state >= EntryState::Parsed {
             let host = AnalysisHost::new();
-            let (_, linearized) = typecheck::type_check(t, global_env, self, host)?;
+            let (_, linearized) = typecheck::type_check(term, global_env, self, host)?;
             self.update_state(file_id, EntryState::Typechecked);
             lin_cache.insert(file_id, linearized);
             Ok(CacheOp::Done(()))
diff --git a/lsp/nls/src/files.rs b/lsp/nls/src/files.rs
index b61b5e68..370cb3d7 100644
--- a/lsp/nls/src/files.rs
+++ b/lsp/nls/src/files.rs
@@ -3,7 +3,9 @@ use codespan::FileId;
 use codespan_reporting::diagnostic::Diagnostic;
 use log::trace;
 use lsp_server::Notification;
-use lsp_types::{DidChangeTextDocumentParams, DidOpenTextDocumentParams, PublishDiagnosticsParams};
+use lsp_types::{
+    DidChangeTextDocumentParams, DidOpenTextDocumentParams, PublishDiagnosticsParams, Url,
+};
 use nickel::{
     cache::{CacheError, CacheOp},
     error::ToDiagnostic,
@@ -18,38 +20,7 @@ pub fn handle_open(server: &mut Server, params: DidOpenTextDocumentParams) -> Re
         .cache
         .add_string(params.text_document.uri.as_str(), params.text_document.text);
 
-    let checked = server
-        .cache
-        .parse(file_id)
-        .map_err(|parse_err| parse_err.to_diagnostic(server.cache.files_mut(), None))
-        .and_then(|_| {
-            trace!("Parsed, checking types");
-            typecheck(server, file_id)
-        })
-        .map_err(|diagnostics| {
-            trace!("Parsing or typechecking caused an error!");
-            diagnostics
-                .into_iter()
-                .map(|d| lsp_types::Diagnostic::from_codespan(d, server.cache.files_mut()))
-                .flatten()
-                .collect()
-        });
-
-    match checked.map(|_| Vec::new()) {
-        Ok(diagnostics) | Err(diagnostics) => {
-            server.notify(Notification::new(
-                "textDocument/publishDiagnostics".into(),
-                PublishDiagnosticsParams {
-                    uri: params.text_document.uri,
-
-                    diagnostics,
-                    version: None,
-                },
-            ));
-        }
-    }
-
-    Ok(())
+    parse_and_typecheck(server, params.text_document.uri, file_id)
 }
 
 pub fn handle_save(server: &mut Server, params: DidChangeTextDocumentParams) -> Result<()> {
@@ -61,37 +32,7 @@ pub fn handle_save(server: &mut Server, params: DidChangeTextDocumentParams) ->
     // TODO: make this part more abstracted
     //       implement typecheck (at least) as part of a persistent AST representation
     //       for now execute the same as above for handling `open` notifications
-    let checked = server
-        .cache
-        .parse(file_id)
-        .map_err(|parse_err| parse_err.to_diagnostic(server.cache.files_mut(), None))
-        .and_then(|_| {
-            trace!("Parsed, checking types");
-            typecheck(server, file_id)
-        })
-        .map_err(|diagnostics| {
-            trace!("Parsing or typechecking caused an error!");
-            diagnostics
-                .into_iter()
-                .map(|d| lsp_types::Diagnostic::from_codespan(d, server.cache.files_mut()))
-                .flatten()
-                .collect()
-        });
-
-    match checked.map(|_| Vec::new()) {
-        Ok(diagnostics) | Err(diagnostics) => {
-            server.notify(Notification::new(
-                "textDocument/publishDiagnostics".into(),
-                PublishDiagnosticsParams {
-                    uri: params.text_document.uri,
-                    diagnostics,
-                    version: None,
-                },
-            ));
-        }
-    }
-
-    Ok(())
+    parse_and_typecheck(server, params.text_document.uri, file_id)
 }
 
 fn typecheck(server: &mut Server, file_id: FileId) -> Result<CacheOp<()>, Vec<Diagnostic<FileId>>> {
@@ -103,3 +44,37 @@ fn typecheck(server: &mut Server, file_id: FileId) -> Result<CacheOp<()>, Vec<Di
             CacheError::NotParsed => unreachable!(),
         })
 }
+
+fn parse_and_typecheck(server: &mut Server, uri: Url, file_id: FileId) -> Result<()> {
+    let diagnostics = server
+        .cache
+        .parse(file_id)
+        .map_err(|parse_err| parse_err.to_diagnostic(server.cache.files_mut(), None))
+        .and_then(|parse_errs| {
+            // Parse errors are not fatal
+            let mut d = parse_errs
+                .inner()
+                .to_diagnostic(server.cache.files_mut(), None);
+            trace!("Parsed, checking types");
+            let _ = typecheck(server, file_id).map_err(|mut ty_d| d.append(&mut ty_d));
+            Ok(d)
+        })
+        .unwrap_or_else(|d| d);
+
+    let diagnostics = diagnostics
+        .into_iter()
+        .map(|d| lsp_types::Diagnostic::from_codespan(d, server.cache.files_mut()))
+        .flatten()
+        .collect();
+
+    server.notify(Notification::new(
+        "textDocument/publishDiagnostics".into(),
+        PublishDiagnosticsParams {
+            uri,
+            diagnostics,
+            version: None,
+        },
+    ));
+
+    Ok(())
+}
diff --git a/src/cache.rs b/src/cache.rs
index f5ad4814..3ef353c8 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -1,6 +1,6 @@
 //! Source cache.
 
-use crate::error::{Error, ImportError, ParseError, TypecheckError};
+use crate::error::{Error, ImportError, ParseError, ParseErrors, TypecheckError};
 use crate::parser::lexer::Lexer;
 use crate::position::TermPos;
 use crate::stdlib as nickel_stdlib;
@@ -62,11 +62,19 @@ pub struct Cache {
     /// Map containing for each FileIDs a list of files they import.
     imports: HashMap<FileId, HashSet<FileId>>,
     /// The table storing parsed terms corresponding to the entries of the file database.
-    terms: HashMap<FileId, (RichTerm, EntryState)>,
+    terms: HashMap<FileId, CachedTerm>,
     /// The list of ids corresponding to the stdlib modules
     stdlib_ids: Option<Vec<FileId>>,
 }
 
+#[derive(Debug, Clone, PartialEq)]
+pub struct CachedTerm {
+    pub term: RichTerm,
+    pub state: EntryState,
+    /// Any non fatal parse errors.
+    pub parse_errs: ParseErrors,
+}
+
 /// Cache keys for sources.
 ///
 /// A source can be either a snippet input by the user, in which case it is only identified by its
@@ -121,6 +129,14 @@ pub enum CacheOp<T> {
     Cached(T),
 }
 
+impl<T> CacheOp<T> {
+    pub fn inner(self: CacheOp<T>) -> T {
+        match self {
+            CacheOp::Done(t) | CacheOp::Cached(t) => t,
+        }
+    }
+}
+
 /// Wrapper around other errors to indicate that typechecking or applying program transformations
 /// failed because the source has not been parsed yet.
 #[derive(Eq, PartialEq, Debug, Clone)]
@@ -290,13 +306,20 @@ impl Cache {
 
     /// Parse a source and populate the corresponding entry in the cache, or do nothing if the
     /// entry has already been parsed.
-    pub fn parse(&mut self, file_id: FileId) -> Result<CacheOp<()>, ParseError> {
-        if self.terms.contains_key(&file_id) {
-            Ok(CacheOp::Cached(()))
+    pub fn parse(&mut self, file_id: FileId) -> Result<CacheOp<ParseErrors>, ParseError> {
+        if let Some(CachedTerm { parse_errs, .. }) = self.terms.get(&file_id) {
+            Ok(CacheOp::Cached(parse_errs.clone()))
         } else {
-            self.terms
-                .insert(file_id, (self.parse_nocache(file_id)?, EntryState::Parsed));
-            Ok(CacheOp::Done(()))
+            let (term, parse_errs) = self.parse_nocache(file_id)?;
+            self.terms.insert(
+                file_id,
+                CachedTerm {
+                    term,
+                    state: EntryState::Parsed,
+                    parse_errs: parse_errs.clone(),
+                },
+            );
+            Ok(CacheOp::Done(parse_errs.clone()))
         }
     }
 
@@ -306,23 +329,25 @@ impl Cache {
         &mut self,
         file_id: FileId,
         format: InputFormat,
-    ) -> Result<CacheOp<()>, ParseError> {
-        if self.terms.contains_key(&file_id) {
-            Ok(CacheOp::Cached(()))
+    ) -> Result<CacheOp<ParseErrors>, ParseError> {
+        if let Some(CachedTerm { parse_errs, .. }) = self.terms.get(&file_id) {
+            Ok(CacheOp::Cached(parse_errs.clone()))
         } else {
+            let (term, parse_errs) = self.parse_nocache_multi(file_id, format)?;
             self.terms.insert(
                 file_id,
-                (
-                    self.parse_nocache_multi(file_id, format)?,
-                    EntryState::Parsed,
-                ),
+                CachedTerm {
+                    term,
+                    state: EntryState::Parsed,
+                    parse_errs: parse_errs.clone(),
+                },
             );
-            Ok(CacheOp::Done(()))
+            Ok(CacheOp::Done(parse_errs.clone()))
         }
     }
 
     /// Parse a source without querying nor populating the cache.
-    pub fn parse_nocache(&self, file_id: FileId) -> Result<RichTerm, ParseError> {
+    pub fn parse_nocache(&self, file_id: FileId) -> Result<(RichTerm, ParseErrors), ParseError> {
         self.parse_nocache_multi(file_id, InputFormat::Nickel)
     }
 
@@ -331,22 +356,33 @@ impl Cache {
         &self,
         file_id: FileId,
         format: InputFormat,
-    ) -> Result<RichTerm, ParseError> {
+    ) -> Result<(RichTerm, ParseErrors), ParseError> {
         let buf = self.files.source(file_id);
 
         match format {
             InputFormat::Nickel => {
+                let mut parse_err_rec = Vec::new();
                 let t = parser::grammar::TermParser::new()
-                    .parse(file_id, Lexer::new(&buf))
-                    .map_err(|err| ParseError::from_lalrpop(err, file_id))?;
-                Ok(t)
+                    .parse(file_id, &mut parse_err_rec, Lexer::new(&buf))
+                    .map_err(|err| (ParseError::from_lalrpop(err, file_id).into()))?;
+
+                let parse_errs: ParseErrors = parse_err_rec
+                    .into_iter()
+                    .map(|e| ParseError::from_lalrpop(e.error, file_id))
+                    .collect::<Vec<_>>()
+                    .into();
+
+                Ok((t, parse_errs))
             }
             InputFormat::Json => serde_json::from_str(self.files.source(file_id))
-                .map_err(|err| ParseError::from_serde_json(err, file_id, &self.files)),
+                .map(|t| (t, ParseErrors::default()))
+                .map_err(|err| ParseError::from_serde_json(err, file_id, &self.files).into()),
             InputFormat::Yaml => serde_yaml::from_str(self.files.source(file_id))
-                .map_err(|err| ParseError::from_serde_yaml(err, file_id)),
+                .map(|t| (t, ParseErrors::default()))
+                .map_err(|err| (ParseError::from_serde_yaml(err, file_id).into())),
             InputFormat::Toml => toml::from_str(self.files.source(file_id))
-                .map_err(|err| ParseError::from_toml(err, file_id, &self.files)),
+                .map(|t| (t, ParseErrors::default()))
+                .map_err(|err| (ParseError::from_toml(err, file_id, &self.files).into())),
         }
     }
 
@@ -359,10 +395,12 @@ impl Cache {
         global_env: &eval::Environment,
     ) -> Result<CacheOp<()>, CacheError<TypecheckError>> {
         match self.terms.get(&file_id) {
-            Some((_, state)) if *state >= EntryState::Typechecked => Ok(CacheOp::Cached(())),
-            Some((t, state)) if *state >= EntryState::Parsed => {
+            Some(CachedTerm { state, .. }) if *state >= EntryState::Typechecked => {
+                Ok(CacheOp::Cached(()))
+            }
+            Some(CachedTerm { term, state, .. }) if *state >= EntryState::Parsed => {
                 if *state < EntryState::Typechecking {
-                    type_check(t, global_env, self, StubHost::<(), _>::new())?;
+                    type_check(term, global_env, self, StubHost::<(), _>::new())?;
                     self.update_state(file_id, EntryState::Typechecking);
                 }
 
@@ -388,9 +426,18 @@ impl Cache {
             Some(state) if state >= EntryState::Transformed => Ok(CacheOp::Cached(())),
             Some(state) if state >= EntryState::Parsed => {
                 if state < EntryState::Transforming {
-                    let (t, _) = self.terms.remove(&file_id).unwrap();
-                    let t = transformations::transform(t);
-                    self.terms.insert(file_id, (t, EntryState::Transforming));
+                    let CachedTerm {
+                        term, parse_errs, ..
+                    } = self.terms.remove(&file_id).unwrap();
+                    let term = transformations::transform(term);
+                    self.terms.insert(
+                        file_id,
+                        CachedTerm {
+                            term,
+                            state: EntryState::Transforming,
+                            parse_errs,
+                        },
+                    );
                 }
 
                 if let Some(imports) = self.imports.get(&file_id).cloned() {
@@ -428,10 +475,14 @@ impl Cache {
         match self.entry_state(file_id) {
             Some(state) if state >= EntryState::Transformed => Ok(CacheOp::Cached(())),
             Some(_) => {
-                let (mut t, state) = self.terms.remove(&file_id).unwrap();
+                let CachedTerm {
+                    mut term,
+                    state,
+                    parse_errs,
+                } = self.terms.remove(&file_id).unwrap();
 
                 if state < EntryState::Transforming {
-                    match t.term.as_mut() {
+                    match term.term.as_mut() {
                         Term::Record(ref mut map, _) => {
                             let map_res = std::mem::replace(map, HashMap::new())
                                 .into_iter()
@@ -461,7 +512,14 @@ impl Cache {
                         _ => panic!("cache::transform_inner(): not a record"),
                     }
 
-                    self.terms.insert(file_id, (t, EntryState::Transforming));
+                    self.terms.insert(
+                        file_id,
+                        CachedTerm {
+                            term,
+                            state: EntryState::Transforming,
+                            parse_errs,
+                        },
+                    );
                 }
 
                 if let Some(imports) = self.imports.get(&file_id).cloned() {
@@ -489,10 +547,18 @@ impl Cache {
             Some(state) if state >= EntryState::ImportsResolved => Ok(CacheOp::Cached(())),
             Some(state) if state >= EntryState::Parsed => {
                 if state < EntryState::ImportsResolving {
-                    let (t, _) = self.terms.remove(&file_id).unwrap();
-                    let (t, pending) = transformations::resolve_imports(t, self)?;
-                    self.terms
-                        .insert(file_id, (t, EntryState::ImportsResolving));
+                    let CachedTerm {
+                        term, parse_errs, ..
+                    } = self.terms.remove(&file_id).unwrap();
+                    let (term, pending) = transformations::resolve_imports(term, self)?;
+                    self.terms.insert(
+                        file_id,
+                        CachedTerm {
+                            term,
+                            state: EntryState::ImportsResolving,
+                            parse_errs,
+                        },
+                    );
 
                     for id in pending {
                         self.resolve_imports(id)?;
@@ -522,7 +588,7 @@ impl Cache {
     ) -> Result<CacheOp<()>, Error> {
         let mut result = CacheOp::Cached(());
 
-        if self.parse(file_id)? == CacheOp::Done(()) {
+        if self.parse(file_id)? == CacheOp::Done(ParseErrors::default()) {
             result = CacheOp::Done(());
         };
 
@@ -565,7 +631,10 @@ impl Cache {
         file_id: FileId,
         global_env: &eval::Environment,
     ) -> Result<(RichTerm, Vec<FileId>), Error> {
-        let term = self.parse_nocache(file_id)?;
+        let (term, errs) = self.parse_nocache(file_id)?;
+        if errs.no_errors() {
+            return Err(Error::ParseErrors(errs));
+        }
         let (term, pending) = transformations::resolve_imports(term, self)?;
         type_check(&term, global_env, self, StubHost::<(), _>::new())?;
         let term = transformations::transform(term);
@@ -623,13 +692,13 @@ impl Cache {
 
     /// Get a mutable reference to the cached term roots
     /// (used by the language server to invalidate previously parsed entries)
-    pub fn terms_mut(&mut self) -> &mut HashMap<FileId, (RichTerm, EntryState)> {
+    pub fn terms_mut(&mut self) -> &mut HashMap<FileId, CachedTerm> {
         &mut self.terms
     }
 
     /// Get an immutable reference to the cached term roots
     /// (used by the language server to invalidate previously parsed entries)
-    pub fn terms(&self) -> &HashMap<FileId, (RichTerm, EntryState)> {
+    pub fn terms(&self) -> &HashMap<FileId, CachedTerm> {
         &self.terms
     }
 
@@ -637,23 +706,28 @@ impl Cache {
     pub fn update_state(&mut self, file_id: FileId, new: EntryState) -> Option<EntryState> {
         self.terms
             .get_mut(&file_id)
-            .map(|(_, old)| std::mem::replace(old, new))
+            .map(|CachedTerm { state, .. }| std::mem::replace(state, new))
     }
 
     /// Retrieve the state of an entry. Return `None` if the entry is not in the term cache,
     /// meaning that the content of the source has been loaded but has not been parsed yet.
     pub fn entry_state(&self, file_id: FileId) -> Option<EntryState> {
-        self.terms.get(&file_id).map(|(_, state)| state).copied()
+        self.terms
+            .get(&file_id)
+            .map(|CachedTerm { state, .. }| state)
+            .copied()
     }
 
     /// Retrieve a fresh clone of a cached term.
     pub fn get_owned(&self, file_id: FileId) -> Option<RichTerm> {
-        self.terms.get(&file_id).map(|(t, _)| t.clone())
+        self.terms
+            .get(&file_id)
+            .map(|CachedTerm { term, .. }| term.clone())
     }
 
     /// Retrieve a reference to a cached term.
     pub fn get_ref(&self, file_id: FileId) -> Option<&RichTerm> {
-        self.terms.get(&file_id).map(|(t, _)| t)
+        self.terms.get(&file_id).map(|CachedTerm { term, .. }| term)
     }
 
     /// Load and parse the standard library in the cache.
@@ -817,17 +891,20 @@ impl ImportResolver for Cache {
             }
         };
 
+        // We ignore non fatal parse errors while importing.
         self.parse_multi(file_id, format)
-            .map_err(|err| ImportError::ParseError(err, *pos))?;
+            .map_err(|err| ImportError::ParseErrors(err.into(), *pos))?;
 
         Ok((ResolvedTerm::FromFile { path: path_buf }, file_id))
     }
 
     fn get(&self, file_id: FileId) -> Option<RichTerm> {
-        self.terms.get(&file_id).map(|(term, state)| {
-            debug_assert!(*state >= EntryState::ImportsResolved);
-            term.clone()
-        })
+        self.terms
+            .get(&file_id)
+            .map(|CachedTerm { term, state, .. }| {
+                debug_assert!(*state >= EntryState::ImportsResolved);
+                term.clone()
+            })
     }
 
     fn get_path(&self, file_id: FileId) -> &OsStr {
@@ -931,9 +1008,9 @@ pub mod resolvers {
             } else {
                 let buf = self.files.source(file_id);
                 let term = parser::grammar::TermParser::new()
-                    .parse(file_id, Lexer::new(&buf))
+                    .parse(file_id, &mut Vec::new(), Lexer::new(&buf))
                     .map_err(|e| ParseError::from_lalrpop(e, file_id))
-                    .map_err(|e| ImportError::ParseError(e, *pos))?;
+                    .map_err(|e| ImportError::ParseErrors(e.into(), *pos))?;
                 self.term_cache.insert(file_id, term);
                 Ok((
                     ResolvedTerm::FromFile {
diff --git a/src/error.rs b/src/error.rs
index 4035c9de..59af28fd 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -23,7 +23,7 @@ use crate::{label, repl};
 pub enum Error {
     EvalError(EvalError),
     TypecheckError(TypecheckError),
-    ParseError(ParseError),
+    ParseErrors(ParseErrors),
     ImportError(ImportError),
     SerializationError(SerializationError),
     IOError(IOError),
@@ -189,6 +189,58 @@ pub enum TypecheckError {
     ),
 }
 
+#[derive(Debug, PartialEq, Clone, Default)]
+pub struct ParseErrors {
+    errors: Vec<ParseError>,
+}
+
+impl ParseErrors {
+    pub fn new(errors: Vec<ParseError>) -> ParseErrors {
+        ParseErrors { errors }
+    }
+    pub fn errors(self) -> Option<Vec<ParseError>> {
+        if self.errors.is_empty() {
+            None
+        } else {
+            Some(self.errors)
+        }
+    }
+
+    pub fn no_errors(&self) -> bool {
+        self.errors.is_empty()
+    }
+
+    pub const fn none() -> ParseErrors {
+        ParseErrors { errors: Vec::new() }
+    }
+}
+
+impl From<ParseError> for ParseErrors {
+    fn from(e: ParseError) -> ParseErrors {
+        ParseErrors { errors: vec![e] }
+    }
+}
+
+impl From<Vec<ParseError>> for ParseErrors {
+    fn from(errors: Vec<ParseError>) -> ParseErrors {
+        ParseErrors { errors }
+    }
+}
+
+impl ToDiagnostic<FileId> for ParseErrors {
+    fn to_diagnostic(
+        &self,
+        files: &mut Files<String>,
+        contract_id: Option<FileId>,
+    ) -> Vec<Diagnostic<FileId>> {
+        self.errors
+            .iter()
+            .map(|e| e.to_diagnostic(files, contract_id))
+            .flatten()
+            .collect()
+    }
+}
+
 /// An error occurring during parsing.
 #[derive(Debug, PartialEq, Clone)]
 pub enum ParseError {
@@ -228,8 +280,8 @@ pub enum ImportError {
         /* import position */ TermPos,
     ),
     /// A parse error occurred during an import.
-    ParseError(
-        /* error */ ParseError,
+    ParseErrors(
+        /* error */ ParseErrors,
         /* import position */ TermPos,
     ),
 }
@@ -268,7 +320,15 @@ impl From<EvalError> for Error {
 
 impl From<ParseError> for Error {
     fn from(error: ParseError) -> Error {
-        Error::ParseError(error)
+        Error::ParseErrors(ParseErrors {
+            errors: vec![error],
+        })
+    }
+}
+
+impl From<ParseErrors> for Error {
+    fn from(errors: ParseErrors) -> Error {
+        Error::ParseErrors(errors)
     }
 }
 
@@ -835,7 +895,12 @@ impl ToDiagnostic<FileId> for Error {
         contract_id: Option<FileId>,
     ) -> Vec<Diagnostic<FileId>> {
         match self {
-            Error::ParseError(err) => err.to_diagnostic(files, contract_id),
+            Error::ParseErrors(errs) => errs
+                .errors
+                .iter()
+                .map(|e| e.to_diagnostic(files, contract_id))
+                .flatten()
+                .collect(),
             Error::TypecheckError(err) => err.to_diagnostic(files, contract_id),
             Error::EvalError(err) => err.to_diagnostic(files, contract_id),
             Error::ImportError(err) => err.to_diagnostic(files, contract_id),
@@ -1412,8 +1477,13 @@ impl ToDiagnostic<FileId> for ImportError {
                     .with_message(format!("Import of {} failed: {}", path, error))
                     .with_labels(labels)]
             }
-            ImportError::ParseError(error, span_opt) => {
-                let mut diagnostic = error.to_diagnostic(files, contract_id);
+            ImportError::ParseErrors(error, span_opt) => {
+                let mut diagnostic: Vec<Diagnostic<FileId>> = error
+                    .errors
+                    .iter()
+                    .map(|e| e.to_diagnostic(files, contract_id))
+                    .flatten()
+                    .collect();
 
                 if let Some(span) = span_opt.as_opt_ref() {
                     diagnostic[0]
diff --git a/src/eval.rs b/src/eval.rs
index 0784cff6..77121702 100644
--- a/src/eval.rs
+++ b/src/eval.rs
@@ -857,6 +857,7 @@ pub fn subst(rt: RichTerm, global_env: &Environment, env: &Environment) -> RichT
                 })
                 .unwrap_or_else(|| RichTerm::new(Term::Var(id), pos)),
             v @ Term::Null
+            | v @ Term::ParseError
             | v @ Term::Bool(_)
             | v @ Term::Num(_)
             | v @ Term::Str(_)
@@ -1059,7 +1060,7 @@ mod tests {
         let id = Files::new().add("<test>", String::from(s));
 
         grammar::TermParser::new()
-            .parse(id, lexer::Lexer::new(&s))
+            .parse(id, &mut Vec::new(), lexer::Lexer::new(&s))
             .map(|mut t| {
                 t.clean_pos();
                 t
@@ -1230,7 +1231,7 @@ mod tests {
 
         // let x = import "bad" in x
         match mk_import("x", "bad", mk_term::var("x"), &mut resolver).unwrap_err() {
-            ImportError::ParseError(_, _) => (),
+            ImportError::ParseErrors(_, _) => (),
             _ => assert!(false),
         };
 
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index 94fda32f..1a34f72c 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -3,6 +3,8 @@ use std::ffi::OsString;
 
 use codespan::FileId;
 
+use lalrpop_util::ErrorRecovery;
+
 use super::ExtendedTerm;
 use super::utils::{StringKind, mk_pos, mk_label, mk_span, strip_indent, SwitchCase,
     FieldPathElem, strip_indent_doc, build_record, elaborate_field_path, check_unbound,
@@ -17,7 +19,7 @@ use crate::term::{BinaryOp, RichTerm, Term, UnaryOp, StrChunk, MetaValue,
 use crate::term::make as mk_term;
 use crate::types::{Types, AbsType};
 
-grammar<'input>(src_id: FileId);
+grammar<'input, 'err>(src_id: FileId, errors: &'err mut Vec<ErrorRecovery<usize, Token<'input>, ParseError>>);
 
 WithPos<Rule>: RichTerm = <l: @L> <t: Rule> <r: @R> => t.with_pos(mk_pos(src_id, l, r));
 CheckUnbound<Rule>: Types = <l: @L> <t: Rule> <r: @R> =>? check_unbound(&t, mk_span(src_id, l, r)).map_err(|e| lalrpop_util::ParseError::User{error: e}).and(Ok(t));
@@ -132,6 +134,7 @@ RootTerm: RichTerm = {
     "if" <cond: Term> "then" <t1: Term> "else" <t2: Term> =>
         mk_app!(Term::Op1(UnaryOp::Ite(), cond), t1, t2),
     AnnotatedTerm,
+    <l: @L> <t: !> <r: @R> => { errors.push(t); RichTerm { term: Box::new(Term::ParseError), pos: mk_pos(src_id, l, r)} },
 };
 
 AnnotatedTerm: RichTerm = {
diff --git a/src/program.rs b/src/program.rs
index e8a60aa5..64110730 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -160,7 +160,7 @@ pub fn query(
         let source = format!("x.{}", p);
         let query_file_id = cache.add_tmp("<query>", source.clone());
         let new_term = parser::grammar::TermParser::new()
-            .parse(query_file_id, Lexer::new(&source))
+            .parse(query_file_id, &mut Vec::new(), Lexer::new(&source))
             .map_err(|err| ParseError::from_lalrpop(err, query_file_id))?;
 
         // Substituting `y` for `t`
@@ -219,7 +219,7 @@ mod tests {
         let id = Files::new().add("<test>", String::from(s));
 
         grammar::TermParser::new()
-            .parse(id, lexer::Lexer::new(&s))
+            .parse(id, &mut Vec::new(), lexer::Lexer::new(&s))
             .map(|mut t| {
                 t.clean_pos();
                 t
diff --git a/src/repl/mod.rs b/src/repl/mod.rs
index d28eb844..08d234e3 100644
--- a/src/repl/mod.rs
+++ b/src/repl/mod.rs
@@ -119,7 +119,7 @@ impl REPLImpl {
 
         match self
             .parser
-            .parse(file_id, lexer::Lexer::new(exp))
+            .parse(file_id, &mut Vec::new(), lexer::Lexer::new(exp))
             .map_err(|err| ParseError::from_lalrpop(err, file_id))?
         {
             // Because we don't use the cache for input, we have to perform recursive import
@@ -223,7 +223,8 @@ impl REPL for REPLImpl {
 
     fn typecheck(&mut self, exp: &str) -> Result<Types, Error> {
         let file_id = self.cache.add_tmp("<repl-typecheck>", String::from(exp));
-        let term = self.cache.parse_nocache(file_id)?;
+        // We ignore non fatal errors while type checking.
+        let (term, _) = self.cache.parse_nocache(file_id)?;
         let (term, pending) = transformations::resolve_imports(term, &mut self.cache)?;
         for id in &pending {
             self.cache.resolve_imports(*id).unwrap();
@@ -302,7 +303,7 @@ impl InputParser {
 
         let result = self
             .parser
-            .parse(self.file_id, lexer::Lexer::new(input))
+            .parse(self.file_id, &mut Vec::new(), lexer::Lexer::new(input))
             .map_err(|err| ParseError::from_lalrpop(err, self.file_id));
 
         match result {
diff --git a/src/term.rs b/src/term.rs
index f4f1ad92..a593b43e 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -148,6 +148,8 @@ pub enum Term {
     /// A resolved import (which has already been loaded and parsed).
     #[serde(skip)]
     ResolvedImport(FileId),
+    #[serde(skip)]
+    ParseError,
 }
 
 #[derive(Debug, Eq, PartialEq, Copy, Clone)]
@@ -293,7 +295,7 @@ impl Term {
     {
         use self::Term::*;
         match self {
-            Null => (),
+            Null | ParseError => (),
             Switch(ref mut t, ref mut cases, ref mut def) => {
                 cases.iter_mut().for_each(|c| {
                     let (_, t) = c;
@@ -377,7 +379,8 @@ impl Term {
             | Term::Promise(_, _, _)
             | Term::Import(_)
             | Term::ResolvedImport(_)
-            | Term::StrChunks(_) => None,
+            | Term::StrChunks(_)
+            | Term::ParseError => None,
         }
         .map(String::from)
     }
@@ -433,6 +436,7 @@ impl Term {
                 format!("<{}{}={}>", content, value_label, value)
             }
             Term::Var(Ident(id)) => id.clone(),
+            Term::ParseError => String::from("<parse error>"),
             Term::Let(_, _, _)
             | Term::App(_, _)
             | Term::Switch(..)
@@ -498,7 +502,8 @@ impl Term {
             | Term::Import(_)
             | Term::ResolvedImport(_)
             | Term::StrChunks(_)
-            | Term::RecRecord(..) => false,
+            | Term::RecRecord(..)
+            | Term::ParseError => false,
         }
     }
 
@@ -536,7 +541,8 @@ impl Term {
             | Term::Import(_)
             | Term::ResolvedImport(_)
             | Term::StrChunks(_)
-            | Term::RecRecord(..) => false,
+            | Term::RecRecord(..)
+            | Term::ParseError => false,
         }
     }
 }
@@ -882,7 +888,8 @@ impl RichTerm {
         };
 
         let result = match *term {
-            v @ Term::Null
+            v @ Term::ParseError
+            | v @ Term::Null
             | v @ Term::Bool(_)
             | v @ Term::Num(_)
             | v @ Term::Str(_)
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index 13d2e700..21f93b85 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -552,6 +552,7 @@ fn type_check_<S, E>(
     let RichTerm { term: t, pos } = rt;
     linearizer.add_term(lin, t, *pos, ty.clone());
     match t.as_ref() {
+        Term::ParseError => return Ok(()),
         // null is inferred to be of type Dyn
         Term::Null => unify(state, strict, ty, mk_typewrapper::dynamic())
             .map_err(|err| err.into_typecheck_err(state, rt.pos)),
diff --git a/src/types.rs b/src/types.rs
index 1d09ecd1..5ef9b9e5 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -429,7 +429,9 @@ mod test {
         println!("{}", wrapper);
         let id = Files::new().add("<test>", wrapper.clone());
 
-        let rt = TermParser::new().parse(id, Lexer::new(&wrapper)).unwrap();
+        let rt = TermParser::new()
+            .parse(id, &mut Vec::new(), Lexer::new(&wrapper))
+            .unwrap();
 
         match *rt.term {
             Term::MetaValue(MetaValue { mut contracts, .. }) if contracts.len() == 1 => {


Test patch:
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 0666a8ac..af271b5e 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -13,7 +13,7 @@ fn parse(s: &str) -> Result<RichTerm, ParseError> {
     let id = Files::new().add("<test>", String::from(s));
 
     super::grammar::TermParser::new()
-        .parse(id, Lexer::new(&s))
+        .parse(id, &mut Vec::new(), Lexer::new(&s))
         .map_err(|err| ParseError::from_lalrpop(err, id))
 }
 
diff --git a/tests/typecheck_fail.rs b/tests/typecheck_fail.rs
index 29367c38..9b7f7dbc 100644
--- a/tests/typecheck_fail.rs
+++ b/tests/typecheck_fail.rs
@@ -16,7 +16,7 @@ fn type_check_expr(s: impl std::string::ToString) -> Result<Types, TypecheckErro
     let id = Files::new().add("<test>", s.clone());
     type_check(
         &grammar::TermParser::new()
-            .parse(id, lexer::Lexer::new(&s))
+            .parse(id, &mut Vec::new(), lexer::Lexer::new(&s))
             .unwrap(),
     )
 }