Repository: pest-parser/pest (language: rust)

Issue:
pest_meta is still on 1.0
Switching to pest 2.0 now means both versions are compiled and used because pest_meta is still on 1.0. A bit annoying.


Discussion / hints:
Unfortunately, Cargo doesn't yet support prebuilt binaries, so we're currently keeping 1.0 around in order not have a long compilation chain. I'm also open to ideas on how to bootstrap pest more gracefully than this.

As a side note, I don't think that compiling a few extra not so large crates once is a huge paper-cut, but it would be nice to at least have a way to keep the meta grammar up to date.

Fix patch:
diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 00000000..7cf900c7
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,2 @@
+[alias]
+bootstrap = "run --package pest_bootstrap"
diff --git a/.travis.yml b/.travis.yml
index 113e854d..c86760d8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,6 +27,7 @@ notifications:
 script:
   # Need a custom script because we are using cargo workspaces.
   - |
+      cargo bootstrap && # dynamic build dependency of pest_meta
       cargo build --all --verbose &&
       cargo test --all --verbose &&
       cargo doc --all --verbose
diff --git a/Cargo.toml b/Cargo.toml
index a0db4780..8a0f6885 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,6 @@
 [workspace]
 members = [
+    "bootstrap",
     "derive",
     "generator",
     "grammars",
diff --git a/bootstrap/Cargo.toml b/bootstrap/Cargo.toml
new file mode 100644
index 00000000..0b5abe7f
--- /dev/null
+++ b/bootstrap/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "pest_bootstrap"
+description = "pest bootstrap script"
+version = "0.0.0"
+authors = ["Dragoș Tiselice <dragostiselice@gmail.com>"]
+homepage = "https://pest-parser.github.io/"
+repository = "https://github.com/pest-parser/pest"
+documentation = "https://docs.rs/pest"
+publish = false
+license = "MIT/Apache-2.0"
+
+[dependencies]
+pest_generator = "2.0" # Use the crates-io version, which (should be) known-good
+quote = "0.6.8"
+
+[badges]
+codecov = { repository = "pest-parser/pest" }
+maintenance = { status = "actively-developed" }
+travis-ci = { repository = "pest-parser/pest" }
diff --git a/bootstrap/src/main.rs b/bootstrap/src/main.rs
new file mode 100644
index 00000000..8522ff45
--- /dev/null
+++ b/bootstrap/src/main.rs
@@ -0,0 +1,30 @@
+#[macro_use]
+extern crate quote;
+extern crate pest_generator;
+
+use pest_generator::derive_parser;
+use std::{fs::File, io::prelude::*, path::Path};
+
+fn main() {
+    let pest = Path::new(concat!(
+        env!("CARGO_MANIFEST_DIR"),
+        "/../meta/src/grammar.pest"
+    ));
+    let rs = Path::new(concat!(
+        env!("CARGO_MANIFEST_DIR"),
+        "/../meta/src/grammar.rs"
+    ));
+
+    let derived = {
+        let path = pest.to_string_lossy();
+        let pest = quote! {
+            #[grammar = #path]
+            pub struct PestParser;
+        };
+        derive_parser(pest, false)
+    };
+
+    let mut file = File::create(rs).unwrap();
+
+    writeln!(file, "pub struct PestParser;\n{}", derived,).unwrap();
+}
diff --git a/meta/.gitignore b/meta/.gitignore
new file mode 100644
index 00000000..e8843acd
--- /dev/null
+++ b/meta/.gitignore
@@ -0,0 +1,1 @@
+/src/grammar.rs
diff --git a/meta/Cargo.toml b/meta/Cargo.toml
index 21bad95e..11c4a7fe 100644
--- a/meta/Cargo.toml
+++ b/meta/Cargo.toml
@@ -10,13 +10,16 @@ keywords = ["pest", "parser", "meta", "optimizer"]
 categories = ["parsing"]
 license = "MIT/Apache-2.0"
 readme = "_README.md"
+exclude = ["src/grammar.pest"]
 
 [dependencies]
 maplit = "1.0"
-pest = "1.0"
-pest_derive = "1.0"
+pest = { path = "../pest", version = "2.0" }
 
 [badges]
 codecov = { repository = "pest-parser/pest" }
 maintenance = { status = "actively-developed" }
 travis-ci = { repository = "pest-parser/pest" }
+
+[build-dependencies]
+sha-1 = "0.7.0"
diff --git a/meta/build.rs b/meta/build.rs
new file mode 100644
index 00000000..92e849cf
--- /dev/null
+++ b/meta/build.rs
@@ -0,0 +1,69 @@
+extern crate sha1;
+
+use sha1::{Digest, Sha1};
+use std::env;
+use std::fs::{self, File};
+use std::io::prelude::*;
+use std::path::{Path, PathBuf};
+use std::process::{Command};
+
+fn display_digest(digest: &[u8]) -> String {
+    digest.iter()
+        .map(|byte| format!("{:02x}", byte))
+        .collect()
+}
+
+fn main() {
+    println!("rerun-if-changed=src/grammar.pest");
+
+    // Yes; build.rs is supposed to treat `src` as read-only; however:
+    // We want to publish `grammar.rs` and not `grammar.pest`,
+    // so putting it in `src` is the simplest way to do so.
+    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
+    let grammar_pest_path = manifest_dir.join("src/grammar.pest");
+    let grammar_rs_path = manifest_dir.join("src/grammar.rs");
+    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
+    let hash_path = out_dir.join("pest_hash.sha1");
+
+    // If `grammar.pest` exists (we're building from git sources)
+    if grammar_pest_path.exists() {
+        let mut sha = Sha1::default();
+
+        let old_hash = File::open(&hash_path).ok().map(|mut file| {
+            let mut s = String::new();
+            file.read_to_string(&mut s).unwrap();
+            s
+        });
+        let current_grammar = fs::read_to_string(grammar_pest_path).unwrap();
+        sha.input(current_grammar.as_bytes());
+        let current_hash = display_digest(&sha.result());
+
+        // If `grammar.pest` has changed
+        if !grammar_rs_path.exists()
+            || old_hash.as_ref().map(|it| it.trim()) != Some(current_hash.trim())
+        {
+            println!("Bootstrapping `meta/src/grammar.rs`");
+
+            let mut hash_file = File::create(hash_path).unwrap();
+            writeln!(hash_file, "{}", current_hash).unwrap();
+
+            // This "dynamic linking" is probably so fragile I don't even want to hear it
+            let status = Command::new(manifest_dir.join("../target/debug/pest_bootstrap"))
+                .spawn().unwrap_or_else(|_| {
+                    panic!(
+                        "Bootstrap failed because no bootstrap executable was found. \
+                        Please run `cargo build --package pest_bootstrap` or `cargo bootstrap` \
+                        and then try again.",
+                    )
+                })
+                .wait().unwrap();
+            if !status.success() {
+                panic!("Bootstrap failed");
+            }
+        } else {
+            println!("       Fresh `meta/src/grammar.rs`");
+        }
+    } else {
+        assert!(grammar_rs_path.exists(), "package is broken; does not contain grammar.rs");
+    }
+}
diff --git a/meta/src/grammar.pest b/meta/src/grammar.pest
index 93619c9d..bf5da55a 100644
--- a/meta/src/grammar.pest
+++ b/meta/src/grammar.pest
@@ -7,7 +7,7 @@
 // option. All files in the project carrying such notice may not be copied,
 // modified, or distributed except according to those terms.
 
-grammar_rules = _{ soi ~ grammar_rule+ ~ eoi }
+grammar_rules = _{ SOI ~ grammar_rule+ ~ EOI }
 
 grammar_rule = {
     identifier ~ assignment_operator ~ modifier? ~
@@ -77,8 +77,8 @@ insensitive_string =  { "^" ~ string }
 range              =  { character ~ range_operator ~ character }
 character          = ${ single_quote ~ inner_chr ~ single_quote }
 
-inner_str = @{ (!("\"" | "\\") ~ any)* ~ (escape ~ inner_str)? }
-inner_chr = @{ escape | any }
+inner_str = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ inner_str)? }
+inner_chr = @{ escape | ANY }
 escape    = @{ "\\" ~ ("\"" | "\\" | "r" | "n" | "t" | "0" | "'" | code | unicode) }
 code      = @{ "x" ~ hex_digit{2} }
 unicode   = @{ "u" ~ opening_brace ~ hex_digit{2, 6} ~ closing_brace }
@@ -89,5 +89,5 @@ single_quote   = { "'" }
 range_operator = { ".." }
 
 newline    = _{ "\n" | "\r\n" }
-whitespace = _{ " " | "\t" | newline }
-comment    = _{ "//" ~ (!newline ~ any)* }
+WHITESPACE = _{ " " | "\t" | newline }
+COMMENT    = _{ "//" ~ (!newline ~ ANY)* }
diff --git a/meta/src/lib.rs b/meta/src/lib.rs
index 5a810160..e3adb11f 100644
--- a/meta/src/lib.rs
+++ b/meta/src/lib.rs
@@ -13,8 +13,6 @@ extern crate maplit;
 extern crate pest;
 #[cfg(not(test))]
 extern crate pest;
-#[macro_use]
-extern crate pest_derive;
 
 use std::fmt::Display;
 
diff --git a/meta/src/parser.rs b/meta/src/parser.rs
index db39ec35..841b171f 100644
--- a/meta/src/parser.rs
+++ b/meta/src/parser.rs
@@ -10,17 +10,15 @@
 use std::char;
 use std::iter::Peekable;
 
-use pest::{Error, Parser};
-use pest::Span;
 use pest::iterators::{Pair, Pairs};
 use pest::prec_climber::{Assoc, Operator, PrecClimber};
+use pest::{Span, Parser};
+use pest::error::{Error, ErrorVariant};
 
 use ast::{Expr, Rule as AstRule, RuleType};
 use validator;
 
-#[derive(Parser)]
-#[grammar = "grammar.pest"]
-pub struct PestParser;
+include!("grammar.rs");
 
 pub fn parse<'i>(rule: Rule, data: &'i str) -> Result<Pairs<Rule>, Error<Rule>> {
     PestParser::parse(rule, data)
@@ -164,7 +162,7 @@ fn convert_node<'i>(node: ParserNode<'i>) -> Expr {
     }
 }
 
-pub fn consume_rules<'i>(pairs: Pairs<'i, Rule>) -> Result<Vec<AstRule>, Vec<Error<'i, Rule>>> {
+pub fn consume_rules<'i>(pairs: Pairs<'i, Rule>) -> Result<Vec<AstRule>, Vec<Error<Rule>>> {
     let rules = consume_rules_with_spans(pairs)?;
     let errors = validator::validate_ast(&rules);
     if errors.len() == 0 {
@@ -176,7 +174,7 @@ pub fn consume_rules<'i>(pairs: Pairs<'i, Rule>) -> Result<Vec<AstRule>, Vec<Err
 
 fn consume_rules_with_spans<'i>(
     pairs: Pairs<'i, Rule>
-) -> Result<Vec<ParserRule<'i>>, Vec<Error<'i, Rule>>> {
+) -> Result<Vec<ParserRule<'i>>, Vec<Error<Rule>>> {
     let climber = PrecClimber::new(vec![
         Operator::new(Rule::choice_operator, Assoc::Left),
         Operator::new(Rule::sequence_operator, Assoc::Left),
@@ -221,11 +219,11 @@ fn consume_rules_with_spans<'i>(
 fn consume_expr<'i>(
     pairs: Peekable<Pairs<'i, Rule>>,
     climber: &PrecClimber<Rule>
-) -> Result<ParserNode<'i>, Vec<Error<'i, Rule>>> {
+) -> Result<ParserNode<'i>, Vec<Error<Rule>>> {
     fn unaries<'i>(
         mut pairs: Peekable<Pairs<'i, Rule>>,
         climber: &PrecClimber<Rule>
-    ) -> Result<ParserNode<'i>, Vec<Error<'i, Rule>>> {
+    ) -> Result<ParserNode<'i>, Vec<Error<Rule>>> {
         let pair = pairs.next().unwrap();
 
         let node = match pair.as_rule() {
@@ -314,7 +312,7 @@ fn consume_expr<'i>(
 
                 pairs.fold(
                     Ok(node),
-                    |node: Result<ParserNode<'i>, Vec<Error<'i, Rule>>>, pair| {
+                    |node: Result<ParserNode<'i>, Vec<Error<Rule>>>, pair| {
                         let node = node?;
 
                         let node = match pair.as_rule() {
@@ -348,19 +346,21 @@ fn consume_expr<'i>(
                                 let num = if let Ok(num) = number.as_str().parse::<u32>() {
                                     num
                                 } else {
-                                    return Err(vec![
-                                        Error::CustomErrorSpan {
-                                            message: "number cannot overflow u32".to_owned(),
-                                            span: number.into_span()
+                                    return Err(vec![Error::new_from_span(
+                                        ErrorVariant::CustomError {
+                                            message: "number cannot overflow u32".to_owned()
                                         },
-                                    ]);
+                                        number.into_span()
+                                    )]);
                                 };
 
                                 if num == 0 {
-                                    let error: Error<Rule> = Error::CustomErrorSpan {
-                                        message: "cannot repeat 0 times".to_owned(),
-                                        span: number.into_span()
-                                    };
+                                    let error: Error<Rule> = Error::new_from_span(
+                                        ErrorVariant::CustomError {
+                                            message: "cannot repeat 0 times".to_owned()
+                                        },
+                                        number.into_span()
+                                    );
 
                                     return Err(vec![error]);
                                 }
@@ -380,12 +380,12 @@ fn consume_expr<'i>(
                                 let min = if let Ok(min) = min_number.as_str().parse::<u32>() {
                                     min
                                 } else {
-                                    return Err(vec![
-                                        Error::CustomErrorSpan {
-                                            message: "number cannot overflow u32".to_owned(),
-                                            span: min_number.into_span()
+                                    return Err(vec![Error::new_from_span(
+                                        ErrorVariant::CustomError {
+                                            message: "number cannot overflow u32".to_owned()
                                         },
-                                    ]);
+                                        min_number.into_span()
+                                    )]);
                                 };
 
                                 let start = node.span.start_pos();
@@ -404,19 +404,21 @@ fn consume_expr<'i>(
                                 let max = if let Ok(max) = max_number.as_str().parse::<u32>() {
                                     max
                                 } else {
-                                    return Err(vec![
-                                        Error::CustomErrorSpan {
-                                            message: "number cannot overflow u32".to_owned(),
-                                            span: max_number.into_span()
+                                    return Err(vec![Error::new_from_span(
+                                        ErrorVariant::CustomError {
+                                            message: "number cannot overflow u32".to_owned()
                                         },
-                                    ]);
+                                        max_number.into_span()
+                                    )]);
                                 };
 
                                 if max == 0 {
-                                    let error: Error<Rule> = Error::CustomErrorSpan {
-                                        message: "cannot repeat 0 times".to_owned(),
-                                        span: max_number.into_span()
-                                    };
+                                    let error: Error<Rule> = Error::new_from_span(
+                                        ErrorVariant::CustomError {
+                                            message: "cannot repeat 0 times".to_owned()
+                                        },
+                                        max_number.into_span()
+                                    );
 
                                     return Err(vec![error]);
                                 }
@@ -436,12 +438,12 @@ fn consume_expr<'i>(
                                 let min = if let Ok(min) = min_number.as_str().parse::<u32>() {
                                     min
                                 } else {
-                                    return Err(vec![
-                                        Error::CustomErrorSpan {
-                                            message: "number cannot overflow u32".to_owned(),
-                                            span: min_number.into_span()
+                                    return Err(vec![Error::new_from_span(
+                                        ErrorVariant::CustomError {
+                                            message: "number cannot overflow u32".to_owned()
                                         },
-                                    ]);
+                                        min_number.into_span()
+                                    )]);
                                 };
 
                                 inner.next().unwrap(); // comma
@@ -450,19 +452,21 @@ fn consume_expr<'i>(
                                 let max = if let Ok(max) = max_number.as_str().parse::<u32>() {
                                     max
                                 } else {
-                                    return Err(vec![
-                                        Error::CustomErrorSpan {
-                                            message: "number cannot overflow u32".to_owned(),
-                                            span: max_number.into_span()
+                                    return Err(vec![Error::new_from_span(
+                                        ErrorVariant::CustomError {
+                                            message: "number cannot overflow u32".to_owned()
                                         },
-                                    ]);
+                                        max_number.into_span()
+                                    )]);
                                 };
 
                                 if max == 0 {
-                                    let error: Error<Rule> = Error::CustomErrorSpan {
-                                        message: "cannot repeat 0 times".to_owned(),
-                                        span: max_number.into_span()
-                                    };
+                                    let error: Error<Rule> = Error::new_from_span(
+                                        ErrorVariant::CustomError {
+                                            message: "cannot repeat 0 times".to_owned()
+                                        },
+                                        max_number.into_span()
+                                    );
 
                                     return Err(vec![error]);
                                 }
@@ -494,9 +498,9 @@ fn consume_expr<'i>(
     }
 
     let term = |pair: Pair<'i, Rule>| unaries(pair.into_inner().peekable(), climber);
-    let infix = |lhs: Result<ParserNode<'i>, Vec<Error<'i, Rule>>>,
+    let infix = |lhs: Result<ParserNode<'i>, Vec<Error<Rule>>>,
                  op: Pair<'i, Rule>,
-                 rhs: Result<ParserNode<'i>, Vec<Error<'i, Rule>>>| match op.as_rule(
+                 rhs: Result<ParserNode<'i>, Vec<Error<Rule>>>| match op.as_rule(
     ) {
         Rule::sequence_operator => {
             let lhs = lhs?;
diff --git a/meta/src/validator.rs b/meta/src/validator.rs
index 04b3ce1c..af139029 100644
--- a/meta/src/validator.rs
+++ b/meta/src/validator.rs
@@ -9,14 +9,14 @@
 
 use std::collections::{HashMap, HashSet};
 
-use pest::Error;
-use pest::Span;
+use pest::error::{Error, ErrorVariant, InputLocation};
 use pest::iterators::Pairs;
+use pest::Span;
 
 use parser::{ParserExpr, ParserNode, ParserRule, Rule};
 use UNICODE_PROPERTY_NAMES;
 
-pub fn validate_pairs<'i>(pairs: Pairs<'i, Rule>) -> Result<Vec<&'i str>, Vec<Error<'i, Rule>>> {
+pub fn validate_pairs<'i>(pairs: Pairs<'i, Rule>) -> Result<Vec<&'i str>, Vec<Error<Rule>>> {
     let mut rust_keywords = HashSet::new();
     rust_keywords.insert("abstract");
     rust_keywords.insert("alignof");
@@ -144,17 +144,19 @@ pub fn validate_pairs<'i>(pairs: Pairs<'i, Rule>) -> Result<Vec<&'i str>, Vec<Er
 pub fn validate_rust_keywords<'i>(
     definitions: &Vec<Span<'i>>,
     rust_keywords: &HashSet<&str>
-) -> Vec<Error<'i, Rule>> {
+) -> Vec<Error<Rule>> {
     let mut errors = vec![];
 
     for definition in definitions {
         let name = definition.as_str();
 
         if rust_keywords.contains(name) {
-            errors.push(Error::CustomErrorSpan {
-                message: format!("{} is a rust keyword", name),
-                span: definition.clone()
-            })
+            errors.push(Error::new_from_span(
+                ErrorVariant::CustomError {
+                    message: format!("{} is a rust keyword", name)
+                },
+                definition.clone()
+            ))
         }
     }
 
@@ -164,24 +166,26 @@ pub fn validate_rust_keywords<'i>(
 pub fn validate_pest_keywords<'i>(
     definitions: &Vec<Span<'i>>,
     pest_keywords: &HashSet<&str>
-) -> Vec<Error<'i, Rule>> {
+) -> Vec<Error<Rule>> {
     let mut errors = vec![];
 
     for definition in definitions {
         let name = definition.as_str();
 
         if pest_keywords.contains(name) {
-            errors.push(Error::CustomErrorSpan {
-                message: format!("{} is a pest keyword", name),
-                span: definition.clone()
-            })
+            errors.push(Error::new_from_span(
+                ErrorVariant::CustomError {
+                    message: format!("{} is a pest keyword", name)
+                },
+                definition.clone()
+            ))
         }
     }
 
     errors
 }
 
-pub fn validate_already_defined<'i>(definitions: &Vec<Span<'i>>) -> Vec<Error<'i, Rule>> {
+pub fn validate_already_defined<'i>(definitions: &Vec<Span<'i>>) -> Vec<Error<Rule>> {
     let mut errors = vec![];
     let mut defined = HashSet::new();
 
@@ -189,10 +193,12 @@ pub fn validate_already_defined<'i>(definitions: &Vec<Span<'i>>) -> Vec<Error<'i
         let name = definition.as_str();
 
         if defined.contains(&name) {
-            errors.push(Error::CustomErrorSpan {
-                message: format!("rule {} already defined", name),
-                span: definition.clone()
-            })
+            errors.push(Error::new_from_span(
+                ErrorVariant::CustomError {
+                    message: format!("rule {} already defined", name)
+                },
+                definition.clone()
+            ))
         } else {
             defined.insert(name);
         }
@@ -205,7 +211,7 @@ pub fn validate_undefined<'i>(
     definitions: &Vec<Span<'i>>,
     called_rules: &Vec<Span<'i>>,
     builtins: &HashSet<&str>
-) -> Vec<Error<'i, Rule>> {
+) -> Vec<Error<Rule>> {
     let mut errors = vec![];
     let definitions: HashSet<_> = definitions.iter().map(|span| span.as_str()).collect();
 
@@ -213,17 +219,19 @@ pub fn validate_undefined<'i>(
         let name = rule.as_str();
 
         if !definitions.contains(name) && !builtins.contains(name) {
-            errors.push(Error::CustomErrorSpan {
-                message: format!("rule {} is undefined", name),
-                span: rule.clone()
-            })
+            errors.push(Error::new_from_span(
+                ErrorVariant::CustomError {
+                    message: format!("rule {} is undefined", name)
+                },
+                rule.clone()
+            ))
         }
     }
 
     errors
 }
 
-pub fn validate_ast<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<'i, Rule>> {
+pub fn validate_ast<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<Rule>> {
     let mut errors = vec![];
 
     errors.extend(validate_repetition(rules));
@@ -231,8 +239,8 @@ pub fn validate_ast<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<'i,
     errors.extend(validate_whitespace_comment(rules));
     errors.extend(validate_left_recursion(rules));
 
-    errors.sort_by_key(|error| match *error {
-        Error::CustomErrorSpan { ref span, .. } => span.clone(),
+    errors.sort_by_key(|error| match &error.location {
+        InputLocation::Span(span) => span.clone(),
         _ => unreachable!()
     });
 
@@ -309,7 +317,7 @@ fn is_non_failing<'i>(
     }
 }
 
-fn validate_repetition<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<'i, Rule>> {
+fn validate_repetition<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<Rule>> {
     let mut result = vec![];
     let map = to_hash_map(rules);
 
@@ -321,20 +329,25 @@ fn validate_repetition<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<
                 | ParserExpr::RepOnce(ref other)
                 | ParserExpr::RepMin(ref other, _) => {
                     if is_non_failing(&other.expr, &map, &mut vec![]) {
-                        Some(Error::CustomErrorSpan {
-                            message: "expression inside repetition cannot fail and will repeat \
-                                      infinitely"
-                                .to_owned(),
-                            span: node.span.clone()
-                        })
+                        Some(Error::new_from_span(
+                            ErrorVariant::CustomError {
+                                message:
+                                    "expression inside repetition cannot fail and will repeat \
+                                     infinitely"
+                                        .to_owned()
+                            },
+                            node.span.clone()
+                        ))
                     } else if is_non_progressing(&other.expr, &map, &mut vec![]) {
-                        Some(Error::CustomErrorSpan {
-                            message:
-                                "expression inside repetition is non-progressing and will repeat \
-                                 infinitely"
-                                    .to_owned(),
-                            span: node.span.clone()
-                        })
+                        Some(Error::new_from_span(
+                            ErrorVariant::CustomError {
+                                message:
+                                    "expression inside repetition is non-progressing and will repeat \
+                                     infinitely"
+                                        .to_owned(),
+                            },
+                            node.span.clone()
+                        ))
                     } else {
                         None
                     }
@@ -348,12 +361,13 @@ fn validate_repetition<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<
     result
 }
 
-fn validate_choices<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<'i, Rule>> {
+fn validate_choices<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<Rule>> {
     let mut result = vec![];
     let map = to_hash_map(rules);
 
     for rule in rules.into_iter() {
-        let mut errors = rule.node
+        let mut errors = rule
+            .node
             .clone()
             .filter_map_top_down(|node| match node.expr {
                 ParserExpr::Choice(ref lhs, _) => {
@@ -363,12 +377,14 @@ fn validate_choices<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<'i,
                     };
 
                     if is_non_failing(&node.expr, &map, &mut vec![]) {
-                        Some(Error::CustomErrorSpan {
-                            message:
-                                "expression cannot fail; following choices cannot be reached"
-                                    .to_owned(),
-                            span: node.span.clone()
-                        })
+                        Some(Error::new_from_span(
+                            ErrorVariant::CustomError {
+                                message:
+                                    "expression cannot fail; following choices cannot be reached"
+                                        .to_owned()
+                            },
+                            node.span.clone()
+                        ))
                     } else {
                         None
                     }
@@ -382,7 +398,7 @@ fn validate_choices<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<'i,
     result
 }
 
-fn validate_whitespace_comment<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<'i, Rule>> {
+fn validate_whitespace_comment<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<Rule>> {
     let map = to_hash_map(rules);
 
     rules
@@ -390,21 +406,25 @@ fn validate_whitespace_comment<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Ve
         .filter_map(|rule| {
             if rule.name == "whitespace" || rule.name == "comment" {
                 if is_non_failing(&rule.node.expr, &map, &mut vec![]) {
-                    Some(Error::CustomErrorSpan {
-                        message: format!(
-                            "{} cannot fail and will repeat infinitely",
-                            &rule.name
-                        ),
-                        span: rule.node.span.clone()
-                    })
+                    Some(Error::new_from_span(
+                        ErrorVariant::CustomError {
+                            message: format!(
+                                "{} cannot fail and will repeat infinitely",
+                                &rule.name
+                            )
+                        },
+                        rule.node.span.clone()
+                    ))
                 } else if is_non_progressing(&rule.node.expr, &map, &mut vec![]) {
-                    Some(Error::CustomErrorSpan {
-                        message: format!(
-                            "{} is non-progressing and will repeat infinitely",
-                            &rule.name
-                        ),
-                        span: rule.node.span.clone()
-                    })
+                    Some(Error::new_from_span(
+                        ErrorVariant::CustomError {
+                            message: format!(
+                                "{} is non-progressing and will repeat infinitely",
+                                &rule.name
+                            )
+                        },
+                        rule.node.span.clone()
+                    ))
                 } else {
                     None
                 }
@@ -415,7 +435,7 @@ fn validate_whitespace_comment<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Ve
         .collect()
 }
 
-fn validate_left_recursion<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<'i, Rule>> {
+fn validate_left_recursion<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> Vec<Error<Rule>> {
     left_recursion(to_hash_map(rules))
 }
 
@@ -423,12 +443,12 @@ fn to_hash_map<'a, 'i: 'a>(rules: &'a Vec<ParserRule<'i>>) -> HashMap<String, &'
     rules.iter().map(|r| (r.name.clone(), &r.node)).collect()
 }
 
-fn left_recursion<'a, 'i: 'a>(rules: HashMap<String, &'a ParserNode<'i>>) -> Vec<Error<'i, Rule>> {
+fn left_recursion<'a, 'i: 'a>(rules: HashMap<String, &'a ParserNode<'i>>) -> Vec<Error<Rule>> {
     fn check_expr<'a, 'i: 'a>(
         node: &'a ParserNode<'i>,
         rules: &'a HashMap<String, &ParserNode<'i>>,
         trace: &mut Vec<String>
-    ) -> Option<Error<'i, Rule>> {
+    ) -> Option<Error<Rule>> {
         match node.expr.clone() {
             ParserExpr::Ident(other) => {
                 if trace[0] == other {
@@ -439,15 +459,17 @@ fn left_recursion<'a, 'i: 'a>(rules: HashMap<String, &'a ParserNode<'i>>) -> Vec
                         .collect::<Vec<_>>()
                         .join(" -> ");
 
-                    return Some(Error::CustomErrorSpan {
-                        message: format!(
-                            "rule {} is left-recursive ({}); pest::prec_climber might be useful \
-                             in this case",
-                            node.span.as_str(),
-                            chain
-                        ),
-                        span: node.span.clone()
-                    });
+                    return Some(Error::new_from_span(
+                        ErrorVariant::CustomError {
+                            message: format!(
+                                "rule {} is left-recursive ({}); pest::prec_climber might be useful \
+                                 in this case",
+                                node.span.as_str(),
+                                chain
+                            )
+                        },
+                        node.span.clone()
+                    ));
                 }
 
                 if !trace.contains(&other) {


Test patch: