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

Issue:
make PrecClimber support prefix and postfix operators
## Problem

The PrecClimber handles precedence between infix operators, like `a + b * c`, but doesn't handle precedence between prefix or postfix operators, like `-a.b + c`.

## Expectation

The PrecClimber should be able to resolve prefix and postfix operators along with infix operators.


Discussion / hints:
I'm looking at taking a shot at this. The plan is to add awareness of prefix or postfix to the [Assoc enum](https://github.com/pest-parser/pest/blob/7dee2a7770daa213b45f88e6af730868f764927a/pest/src/prec_climber.rs#L101-L110) or the [Operator struct](https://github.com/pest-parser/pest/blob/7dee2a7770daa213b45f88e6af730868f764927a/pest/src/prec_climber.rs#L112-L120).

The Operator seems better but not sure if that will cause backwards compatibility issues and maybe the Assoc is less disruptive to the interface.
See also #344, https://crates.io/crates/pratt

Fix patch:
diff --git a/derive/Cargo.toml b/derive/Cargo.toml
index afa4727b..accff558 100644
--- a/derive/Cargo.toml
+++ b/derive/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "pest_derive"
 description = "pest's derive macro"
-version = "2.3.1"
+version = "2.4.0"
 edition = "2018"
 authors = ["Dragoș Tiselice <dragostiselice@gmail.com>"]
 homepage = "https://pest-parser.github.io/"
@@ -23,5 +23,5 @@ std = ["pest/std", "pest_generator/std"]
 
 [dependencies]
 # for tests, included transitively anyway
-pest = { path = "../pest", version = "2.3.1", default-features = false }
-pest_generator = { path = "../generator", version = "2.3.1", default-features = false }
+pest = { path = "../pest", version = "2.4.0", default-features = false }
+pest_generator = { path = "../generator", version = "2.4.0", default-features = false }
diff --git a/derive/examples/calc.pest b/derive/examples/calc.pest
new file mode 100644
index 00000000..9f2cc3b7
--- /dev/null
+++ b/derive/examples/calc.pest
@@ -0,0 +1,16 @@
+WHITESPACE   =  _{ " " | "\t" | NEWLINE }
+
+ program      =   { SOI ~ expr ~ EOI }
+   expr       =   { prefix* ~ primary ~ postfix* ~ (infix ~ prefix* ~ primary ~ postfix* )* }
+     infix    =  _{ add | sub | mul | div | pow }
+       add    =   { "+" } // Addition
+       sub    =   { "-" } // Subtraction
+       mul    =   { "*" } // Multiplication
+       div    =   { "/" } // Division
+       pow    =   { "^" } // Exponentiation
+     prefix   =  _{ neg }
+       neg    =   { "-" } // Negation
+     postfix  =  _{ fac }
+       fac    =   { "!" } // Factorial
+     primary  =  _{ int | "(" ~ expr ~ ")" }
+       int    =  @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT+ | ASCII_DIGIT) }
\ No newline at end of file
diff --git a/derive/examples/calc.rs b/derive/examples/calc.rs
new file mode 100644
index 00000000..efc6b7b2
--- /dev/null
+++ b/derive/examples/calc.rs
@@ -0,0 +1,109 @@
+mod parser {
+    use pest_derive::Parser;
+
+    #[derive(Parser)]
+    #[grammar = "../examples/calc.pest"]
+    pub struct Parser;
+}
+
+use parser::Rule;
+use pest::{
+    iterators::Pairs,
+    pratt_parser::{Assoc::*, Op, PrattParser},
+    Parser,
+};
+use std::io::{stdin, stdout, Write};
+
+fn parse_to_str(pairs: Pairs<Rule>, pratt: &PrattParser<Rule>) -> String {
+    pratt
+        .map_primary(|primary| match primary.as_rule() {
+            Rule::int => primary.as_str().to_owned(),
+            Rule::expr => parse_to_str(primary.into_inner(), pratt),
+            _ => unreachable!(),
+        })
+        .map_prefix(|op, rhs| match op.as_rule() {
+            Rule::neg => format!("(-{})", rhs),
+            _ => unreachable!(),
+        })
+        .map_postfix(|lhs, op| match op.as_rule() {
+            Rule::fac => format!("({}!)", lhs),
+            _ => unreachable!(),
+        })
+        .map_infix(|lhs, op, rhs| match op.as_rule() {
+            Rule::add => format!("({}+{})", lhs, rhs),
+            Rule::sub => format!("({}-{})", lhs, rhs),
+            Rule::mul => format!("({}*{})", lhs, rhs),
+            Rule::div => format!("({}/{})", lhs, rhs),
+            Rule::pow => format!("({}^{})", lhs, rhs),
+            _ => unreachable!(),
+        })
+        .parse(pairs)
+}
+
+fn parse_to_i32(pairs: Pairs<Rule>, pratt: &PrattParser<Rule>) -> i128 {
+    pratt
+        .map_primary(|primary| match primary.as_rule() {
+            Rule::int => primary.as_str().parse().unwrap(),
+            Rule::expr => parse_to_i32(primary.into_inner(), pratt),
+            _ => unreachable!(),
+        })
+        .map_prefix(|op, rhs| match op.as_rule() {
+            Rule::neg => -rhs,
+            _ => unreachable!(),
+        })
+        .map_postfix(|lhs, op| match op.as_rule() {
+            Rule::fac => (1..lhs + 1).product(),
+            _ => unreachable!(),
+        })
+        .map_infix(|lhs, op, rhs| match op.as_rule() {
+            Rule::add => lhs + rhs,
+            Rule::sub => lhs - rhs,
+            Rule::mul => lhs * rhs,
+            Rule::div => lhs / rhs,
+            Rule::pow => (1..rhs + 1).map(|_| lhs).product(),
+            _ => unreachable!(),
+        })
+        .parse(pairs)
+}
+
+fn main() {
+    let pratt = PrattParser::new()
+        .op(Op::infix(Rule::add, Left) | Op::infix(Rule::sub, Left))
+        .op(Op::infix(Rule::mul, Left) | Op::infix(Rule::div, Left))
+        .op(Op::infix(Rule::pow, Right))
+        .op(Op::postfix(Rule::fac))
+        .op(Op::prefix(Rule::neg));
+
+    let stdin = stdin();
+    let mut stdout = stdout();
+
+    loop {
+        let source = {
+            print!("> ");
+            let _ = stdout.flush();
+            let mut input = String::new();
+            let _ = stdin.read_line(&mut input);
+            input.trim().to_string()
+        };
+
+        let pairs = match parser::Parser::parse(Rule::program, &source) {
+            Ok(mut parse_tree) => {
+                parse_tree
+                    .next()
+                    .unwrap()
+                    .into_inner() // inner of program
+                    .next()
+                    .unwrap()
+                    .into_inner() // inner of expr
+            }
+            Err(err) => {
+                println!("Failed parsing input: {:}", err);
+                continue;
+            }
+        };
+
+        print!("{} => ", source);
+        print!("{} => ", parse_to_str(pairs.clone(), &pratt));
+        println!("{}", parse_to_i32(pairs.clone(), &pratt));
+    }
+}
diff --git a/generator/Cargo.toml b/generator/Cargo.toml
index fded6955..7f0a2a9c 100644
--- a/generator/Cargo.toml
+++ b/generator/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "pest_generator"
 description = "pest code generator"
-version = "2.3.1"
+version = "2.4.0"
 edition = "2018"
 authors = ["Dragoș Tiselice <dragostiselice@gmail.com>"]
 homepage = "https://pest-parser.github.io/"
@@ -18,8 +18,8 @@ default = ["std"]
 std = ["pest/std"]
 
 [dependencies]
-pest = { path = "../pest", version = "2.3.1", default-features = false }
-pest_meta = { path = "../meta", version = "2.3.1" }
+pest = { path = "../pest", version = "2.4.0", default-features = false }
+pest_meta = { path = "../meta", version = "2.4.0" }
 proc-macro2 = "1.0"
 quote = "1.0"
 syn = "1.0"
diff --git a/grammars/Cargo.toml b/grammars/Cargo.toml
index 2c5add13..db1f4645 100644
--- a/grammars/Cargo.toml
+++ b/grammars/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "pest_grammars"
 description = "pest popular grammar implementations"
-version = "2.3.1"
+version = "2.4.0"
 edition = "2018"
 authors = ["Dragoș Tiselice <dragostiselice@gmail.com>"]
 homepage = "https://pest-parser.github.io/"
@@ -14,8 +14,8 @@ readme = "_README.md"
 rust-version = "1.56"
 
 [dependencies]
-pest = { path = "../pest", version = "2.3.1" }
-pest_derive = { path = "../derive", version = "2.3.1" }
+pest = { path = "../pest", version = "2.4.0" }
+pest_derive = { path = "../derive", version = "2.4.0" }
 
 [dev-dependencies]
 criterion = "0.3"
diff --git a/meta/Cargo.toml b/meta/Cargo.toml
index d40aedda..ce174a2d 100644
--- a/meta/Cargo.toml
+++ b/meta/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "pest_meta"
 description = "pest meta language parser and validator"
-version = "2.3.1"
+version = "2.4.0"
 edition = "2018"
 authors = ["Dragoș Tiselice <dragostiselice@gmail.com>"]
 homepage = "https://pest-parser.github.io/"
@@ -16,7 +16,7 @@ include = ["Cargo.toml", "src/**/*", "src/grammar.rs", "_README.md", "LICENSE-*"
 rust-version = "1.56"
 
 [dependencies]
-pest = { path = "../pest", version = "2.3.1" }
+pest = { path = "../pest", version = "2.4.0" }
 once_cell = "1.8.0"
 
 [build-dependencies]
diff --git a/meta/src/parser.rs b/meta/src/parser.rs
index 72abd810..23e2565b 100644
--- a/meta/src/parser.rs
+++ b/meta/src/parser.rs
@@ -12,7 +12,7 @@ use std::iter::Peekable;
 
 use pest::error::{Error, ErrorVariant};
 use pest::iterators::{Pair, Pairs};
-use pest::prec_climber::{Assoc, Operator, PrecClimber};
+use pest::pratt_parser::{Assoc, Op, PrattParser};
 use pest::{Parser, Span};
 
 use crate::ast::{Expr, Rule as AstRule, RuleType};
@@ -175,10 +175,9 @@ pub fn consume_rules(pairs: Pairs<Rule>) -> Result<Vec<AstRule>, Vec<Error<Rule>
 }
 
 fn consume_rules_with_spans(pairs: Pairs<Rule>) -> Result<Vec<ParserRule>, Vec<Error<Rule>>> {
-    let climber = PrecClimber::new(vec![
-        Operator::new(Rule::choice_operator, Assoc::Left),
-        Operator::new(Rule::sequence_operator, Assoc::Left),
-    ]);
+    let pratt = PrattParser::new()
+        .op(Op::infix(Rule::choice_operator, Assoc::Left))
+        .op(Op::infix(Rule::sequence_operator, Assoc::Left));
 
     pairs
         .filter(|pair| pair.as_rule() == Rule::grammar_rule)
@@ -210,7 +209,7 @@ fn consume_rules_with_spans(pairs: Pairs<Rule>) -> Result<Vec<ParserRule>, Vec<E
                 inner_nodes.next().unwrap();
             }
 
-            let node = consume_expr(inner_nodes, &climber)?;
+            let node = consume_expr(inner_nodes, &pratt)?;
 
             Ok(ParserRule {
                 name,
@@ -224,17 +223,17 @@ fn consume_rules_with_spans(pairs: Pairs<Rule>) -> Result<Vec<ParserRule>, Vec<E
 
 fn consume_expr<'i>(
     pairs: Peekable<Pairs<'i, Rule>>,
-    climber: &PrecClimber<Rule>,
+    pratt: &PrattParser<Rule>,
 ) -> Result<ParserNode<'i>, Vec<Error<Rule>>> {
     fn unaries<'i>(
         mut pairs: Peekable<Pairs<'i, Rule>>,
-        climber: &PrecClimber<Rule>,
+        pratt: &PrattParser<Rule>,
     ) -> Result<ParserNode<'i>, Vec<Error<Rule>>> {
         let pair = pairs.next().unwrap();
 
         let node = match pair.as_rule() {
             Rule::opening_paren => {
-                let node = unaries(pairs, climber)?;
+                let node = unaries(pairs, pratt)?;
                 let end = node.span.end_pos();
 
                 ParserNode {
@@ -243,7 +242,7 @@ fn consume_expr<'i>(
                 }
             }
             Rule::positive_predicate_operator => {
-                let node = unaries(pairs, climber)?;
+                let node = unaries(pairs, pratt)?;
                 let end = node.span.end_pos();
 
                 ParserNode {
@@ -252,7 +251,7 @@ fn consume_expr<'i>(
                 }
             }
             Rule::negative_predicate_operator => {
-                let node = unaries(pairs, climber)?;
+                let node = unaries(pairs, pratt)?;
                 let end = node.span.end_pos();
 
                 ParserNode {
@@ -262,14 +261,14 @@ fn consume_expr<'i>(
             }
             other_rule => {
                 let node = match other_rule {
-                    Rule::expression => consume_expr(pair.into_inner().peekable(), climber)?,
+                    Rule::expression => consume_expr(pair.into_inner().peekable(), pratt)?,
                     Rule::_push => {
                         let start = pair.clone().as_span().start_pos();
                         let mut pairs = pair.into_inner();
                         pairs.next().unwrap(); // opening_paren
                         let pair = pairs.next().unwrap();
 
-                        let node = consume_expr(pair.into_inner().peekable(), climber)?;
+                        let node = consume_expr(pair.into_inner().peekable(), pratt)?;
                         let end = node.span.end_pos();
 
                         ParserNode {
@@ -529,7 +528,7 @@ fn consume_expr<'i>(
         Ok(node)
     }
 
-    let term = |pair: Pair<'i, Rule>| unaries(pair.into_inner().peekable(), climber);
+    let term = |pair: Pair<'i, Rule>| unaries(pair.into_inner().peekable(), pratt);
     let infix = |lhs: Result<ParserNode<'i>, Vec<Error<Rule>>>,
                  op: Pair<'i, Rule>,
                  rhs: Result<ParserNode<'i>, Vec<Error<Rule>>>| match op.as_rule() {
@@ -560,7 +559,7 @@ fn consume_expr<'i>(
         _ => unreachable!(),
     };
 
-    climber.climb(pairs, term, infix)
+    pratt.map_primary(term).map_infix(infix).parse(pairs)
 }
 
 fn unescape(string: &str) -> Option<String> {
diff --git a/meta/src/validator.rs b/meta/src/validator.rs
index 5448c3bc..45367795 100644
--- a/meta/src/validator.rs
+++ b/meta/src/validator.rs
@@ -423,7 +423,7 @@ fn left_recursion<'a, 'i: 'a>(rules: HashMap<String, &'a ParserNode<'i>>) -> Vec
                     return Some(Error::new_from_span(
                         ErrorVariant::CustomError {
                             message: format!(
-                                "rule {} is left-recursive ({}); pest::prec_climber might be useful \
+                                "rule {} is left-recursive ({}); pest::pratt_parser might be useful \
                                  in this case",
                                 node.span.as_str(),
                                 chain
@@ -677,7 +677,7 @@ mod tests {
 1 | a = { a }
   |       ^
   |
-  = rule a is left-recursive (a -> a); pest::prec_climber might be useful in this case")]
+  = rule a is left-recursive (a -> a); pest::pratt_parser might be useful in this case")]
     fn simple_left_recursion() {
         let input = "a = { a }";
         unwrap_or_report(consume_rules(
@@ -693,14 +693,14 @@ mod tests {
 1 | a = { b } b = { a }
   |       ^
   |
-  = rule b is left-recursive (b -> a -> b); pest::prec_climber might be useful in this case
+  = rule b is left-recursive (b -> a -> b); pest::pratt_parser might be useful in this case
 
  --> 1:17
   |
 1 | a = { b } b = { a }
   |                 ^
   |
-  = rule a is left-recursive (a -> b -> a); pest::prec_climber might be useful in this case")]
+  = rule a is left-recursive (a -> b -> a); pest::pratt_parser might be useful in this case")]
     fn indirect_left_recursion() {
         let input = "a = { b } b = { a }";
         unwrap_or_report(consume_rules(
@@ -716,7 +716,7 @@ mod tests {
 1 | a = { \"\" ~ \"a\"? ~ \"a\"* ~ (\"a\" | \"\") ~ a }
   |                                       ^
   |
-  = rule a is left-recursive (a -> a); pest::prec_climber might be useful in this case")]
+  = rule a is left-recursive (a -> a); pest::pratt_parser might be useful in this case")]
     fn non_failing_left_recursion() {
         let input = "a = { \"\" ~ \"a\"? ~ \"a\"* ~ (\"a\" | \"\") ~ a }";
         unwrap_or_report(consume_rules(
@@ -732,7 +732,7 @@ mod tests {
 1 | a = { \"a\" | a }
   |             ^
   |
-  = rule a is left-recursive (a -> a); pest::prec_climber might be useful in this case")]
+  = rule a is left-recursive (a -> a); pest::pratt_parser might be useful in this case")]
     fn non_primary_choice_left_recursion() {
         let input = "a = { \"a\" | a }";
         unwrap_or_report(consume_rules(
diff --git a/pest/Cargo.toml b/pest/Cargo.toml
index edd4e3ac..4dbc7dd8 100644
--- a/pest/Cargo.toml
+++ b/pest/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "pest"
 description = "The Elegant Parser"
-version = "2.3.1"
+version = "2.4.0"
 edition = "2018"
 authors = ["Dragoș Tiselice <dragostiselice@gmail.com>"]
 homepage = "https://pest-parser.github.io/"
diff --git a/pest/src/lib.rs b/pest/src/lib.rs
index 30f87db6..c2a83a32 100644
--- a/pest/src/lib.rs
+++ b/pest/src/lib.rs
@@ -90,6 +90,12 @@ mod macros;
 mod parser;
 mod parser_state;
 mod position;
+pub mod pratt_parser;
+#[deprecated(
+    since = "2.4.0",
+    note = "Use `pest::pratt_parser` instead (it is an equivalent which also supports unary prefix/suffix operators).
+While prec_climber is going to be kept in 2.x minor and patch releases, it may be removed in a future major release."
+)]
 pub mod prec_climber;
 mod span;
 mod stack;
diff --git a/pest/src/pratt_parser.rs b/pest/src/pratt_parser.rs
new file mode 100644
index 00000000..fb6be851
--- /dev/null
+++ b/pest/src/pratt_parser.rs
@@ -0,0 +1,379 @@
+// pest. The Elegant Parser
+// Copyright (c) 2018 Dragoș Tiselice
+//
+// Licensed under the Apache License, Version 2.0
+// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
+// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. All files in the project carrying such notice may not be copied,
+// modified, or distributed except according to those terms.
+
+//! Constructs useful in prefix, postfix, and infix operator parsing with the
+//! Pratt parsing method.
+
+use core::iter::Peekable;
+use core::marker::PhantomData;
+use core::ops::BitOr;
+
+use alloc::boxed::Box;
+use alloc::collections::BTreeMap;
+
+use crate::iterators::Pair;
+use crate::RuleType;
+
+/// Associativity of an infix binary operator, used by [`Op::infix(Assoc)`].
+///
+/// [`Op::infix(Assoc)`]: struct.Op.html
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Assoc {
+    /// Left operator associativity. Evaluate expressions from left-to-right.
+    Left,
+    /// Right operator associativity. Evaluate expressions from right-to-left.
+    Right,
+}
+
+type Prec = u32;
+const PREC_STEP: Prec = 10;
+
+/// An operator that corresponds to a rule.
+pub struct Op<R: RuleType> {
+    rule: R,
+    affix: Affix,
+    next: Option<Box<Op<R>>>,
+}
+
+enum Affix {
+    Prefix,
+    Postfix,
+    Infix(Assoc),
+}
+
+impl<R: RuleType> Op<R> {
+    /// Defines `rule` as a prefix unary operator.
+    pub fn prefix(rule: R) -> Self {
+        Self {
+            rule,
+            affix: Affix::Prefix,
+            next: None,
+        }
+    }
+
+    /// Defines `rule` as a postfix unary operator.
+    pub fn postfix(rule: R) -> Self {
+        Self {
+            rule,
+            affix: Affix::Postfix,
+            next: None,
+        }
+    }
+
+    /// Defines `rule` as an infix binary operator with associativity `assoc`.
+    pub fn infix(rule: R, assoc: Assoc) -> Self {
+        Self {
+            rule,
+            affix: Affix::Infix(assoc),
+            next: None,
+        }
+    }
+}
+
+impl<R: RuleType> BitOr for Op<R> {
+    type Output = Self;
+
+    fn bitor(mut self, rhs: Self) -> Self {
+        fn assign_next<R: RuleType>(op: &mut Op<R>, next: Op<R>) {
+            if let Some(ref mut child) = op.next {
+                assign_next(child, next);
+            } else {
+                op.next = Some(Box::new(next));
+            }
+        }
+
+        assign_next(&mut self, rhs);
+        self
+    }
+}
+
+/// Struct containing operators and precedences, which can perform [Pratt parsing][1] on
+/// primary, prefix, postfix and infix expressions over [`Pairs`]. The tokens in [`Pairs`]
+/// should alternate in the order:
+/// `prefix* ~ primary ~ postfix* ~ (infix ~ prefix* ~ primary ~ postfix*)*`
+///
+/// # Panics
+///
+/// Panics will occur when:
+/// * `pairs` is empty
+/// * The tokens in `pairs` does not alternate in the expected order.
+/// * No `map_*` function is specified for a certain kind of operator encountered in `pairs`.
+///
+/// # Example
+///
+/// The following pest grammar defines a calculator which can be used for Pratt parsing.
+///
+/// ```pest
+/// WHITESPACE   =  _{ " " | "\t" | NEWLINE }
+///  
+/// program      =   { SOI ~ expr ~ EOI }
+///   expr       =   { prefix* ~ primary ~ postfix* ~ (infix ~ prefix* ~ primary ~ postfix* )* }
+///     infix    =  _{ add | sub | mul | div | pow }
+///       add    =   { "+" } // Addition
+///       sub    =   { "-" } // Subtraction
+///       mul    =   { "*" } // Multiplication
+///       div    =   { "/" } // Division
+///       pow    =   { "^" } // Exponentiation
+///     prefix   =  _{ neg }
+///       neg    =   { "-" } // Negation
+///     postfix  =  _{ fac }
+///       fac    =   { "!" } // Factorial
+///     primary  =  _{ int | "(" ~ expr ~ ")" }
+///       int    =  @{ (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT+ | ASCII_DIGIT) }
+/// ```
+///
+/// Below is a [`PrattParser`] that is able to parse an `expr` in the above grammar. The order
+/// of precedence corresponds to the order in which [`op`] is called. Thus, `mul` will
+/// have higher precedence than `add`. Operators can also be chained with `|` to give them equal
+/// precedence.
+///
+/// ```
+/// # use pest::pratt_parser::{Assoc, Op, PrattParser};
+/// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+/// # enum Rule { program, expr, int, add, mul, sub, div, pow, fac, neg }
+/// let pratt =
+///     PrattParser::new()
+///         .op(Op::infix(Rule::add, Assoc::Left) | Op::infix(Rule::sub, Assoc::Left))
+///         .op(Op::infix(Rule::mul, Assoc::Left) | Op::infix(Rule::div, Assoc::Left))
+///         .op(Op::infix(Rule::pow, Assoc::Right))
+///         .op(Op::postfix(Rule::fac))
+///         .op(Op::prefix(Rule::neg));
+/// ```
+///
+/// To parse an expression, call the [`map_primary`], [`map_prefix`], [`map_postfix`],
+/// [`map_infix`] and [`parse`] methods as follows:
+///
+/// ```
+/// # use pest::{iterators::Pairs, pratt_parser::PrattParser};
+/// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+/// # enum Rule { program, expr, int, add, mul, sub, div, pow, fac, neg }
+/// fn parse_expr(pairs: Pairs<Rule>, pratt: &PrattParser<Rule>) -> i32 {
+///     pratt
+///         .map_primary(|primary| match primary.as_rule() {
+///             Rule::int  => primary.as_str().parse().unwrap(),
+///             Rule::expr => parse_expr(primary.into_inner(), pratt), // from "(" ~ expr ~ ")"
+///             _          => unreachable!(),
+///         })
+///         .map_prefix(|op, rhs| match op.as_rule() {
+///             Rule::neg  => -rhs,
+///             _          => unreachable!(),
+///         })
+///         .map_postfix(|lhs, op| match op.as_rule() {
+///             Rule::fac  => (1..lhs+1).product(),
+///             _          => unreachable!(),
+///         })
+///         .map_infix(|lhs, op, rhs| match op.as_rule() {
+///             Rule::add  => lhs + rhs,
+///             Rule::sub  => lhs - rhs,
+///             Rule::mul  => lhs * rhs,
+///             Rule::div  => lhs / rhs,
+///             Rule::pow  => (1..rhs+1).map(|_| lhs).product(),
+///             _          => unreachable!(),
+///         })
+///         .parse(pairs)
+/// }
+/// ```
+///
+/// Note that [`map_prefix`], [`map_postfix`] and [`map_infix`] only need to be specified if the
+/// grammar contains the corresponding operators.
+///
+/// [1]: https://en.wikipedia.org/wiki/Pratt_parser
+/// [`Pairs`]: ../iterators/struct.Pairs.html
+/// [`PrattParser`]: struct.PrattParser.html
+/// [`map_primary`]: struct.PrattParser.html#method.map_primary
+/// [`map_prefix`]: struct.PrattParserMap.html#method.map_prefix
+/// [`map_postfix`]: struct.PrattParserMap.html#method.map_postfix
+/// [`map_infix`]: struct.PrattParserMap.html#method.map_infix
+/// [`parse`]: struct.PrattParserMap.html#method.parse
+/// [`op`]: struct.PrattParserMap.html#method.op
+pub struct PrattParser<R: RuleType> {
+    prec: Prec,
+    ops: BTreeMap<R, (Affix, Prec)>,
+    has_prefix: bool,
+    has_postfix: bool,
+    has_infix: bool,
+}
+
+impl<R: RuleType> Default for PrattParser<R> {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl<R: RuleType> PrattParser<R> {
+    /// Instantiate a new `PrattParser`.
+    pub fn new() -> Self {
+        Self {
+            prec: PREC_STEP,
+            ops: BTreeMap::new(),
+            has_prefix: false,
+            has_postfix: false,
+            has_infix: false,
+        }
+    }
+
+    /// Add `op` to `PrattParser`.
+    pub fn op(mut self, op: Op<R>) -> Self {
+        self.prec += PREC_STEP;
+        let mut iter = Some(op);
+        while let Some(Op { rule, affix, next }) = iter.take() {
+            match affix {
+                Affix::Prefix => self.has_prefix = true,
+                Affix::Postfix => self.has_postfix = true,
+                Affix::Infix(_) => self.has_infix = true,
+            }
+            self.ops.insert(rule, (affix, self.prec));
+            iter = next.map(|op| *op);
+        }
+        self
+    }
+
+    /// Maps primary expressions with a closure `primary`.
+    pub fn map_primary<'pratt, 'i, X, T>(
+        &'pratt self,
+        primary: X,
+    ) -> PrattParserMap<'pratt, 'i, R, X, T>
+    where
+        X: FnMut(Pair<'i, R>) -> T,
+        R: 'pratt,
+    {
+        PrattParserMap {
+            pratt: self,
+            primary,
+            prefix: None,
+            postfix: None,
+            infix: None,
+            phantom: PhantomData,
+        }
+    }
+}
+
+type PrefixFn<'i, R, T> = Box<dyn FnMut(Pair<'i, R>, T) -> T + 'i>;
+type PostfixFn<'i, R, T> = Box<dyn FnMut(T, Pair<'i, R>) -> T + 'i>;
+type InfixFn<'i, R, T> = Box<dyn FnMut(T, Pair<'i, R>, T) -> T + 'i>;
+
+/// Product of calling [`map_primary`] on [`PrattParser`], defines how expressions should
+/// be mapped.
+///
+/// [`map_primary`]: struct.PrattParser.html#method.map_primary
+/// [`PrattParser`]: struct.PrattParser.html
+pub struct PrattParserMap<'pratt, 'i, R, F, T>
+where
+    R: RuleType + 'pratt,
+    F: FnMut(Pair<'i, R>) -> T,
+{
+    pratt: &'pratt PrattParser<R>,
+    primary: F,
+    prefix: Option<PrefixFn<'i, R, T>>,
+    postfix: Option<PostfixFn<'i, R, T>>,
+    infix: Option<InfixFn<'i, R, T>>,
+    phantom: PhantomData<T>,
+}
+
+impl<'pratt, 'i, R, F, T> PrattParserMap<'pratt, 'i, R, F, T>
+where
+    R: RuleType + 'pratt,
+    F: FnMut(Pair<'i, R>) -> T,
+{
+    /// Maps prefix operators with closure `prefix`.
+    pub fn map_prefix<X>(mut self, prefix: X) -> Self
+    where
+        X: FnMut(Pair<'i, R>, T) -> T + 'i,
+    {
+        self.prefix = Some(Box::new(prefix));
+        self
+    }
+
+    /// Maps postfix operators with closure `postfix`.
+    pub fn map_postfix<X>(mut self, postfix: X) -> Self
+    where
+        X: FnMut(T, Pair<'i, R>) -> T + 'i,
+    {
+        self.postfix = Some(Box::new(postfix));
+        self
+    }
+
+    /// Maps infix operators with a closure `infix`.
+    pub fn map_infix<X>(mut self, infix: X) -> Self
+    where
+        X: FnMut(T, Pair<'i, R>, T) -> T + 'i,
+    {
+        self.infix = Some(Box::new(infix));
+        self
+    }
+
+    pub fn parse<P: Iterator<Item = Pair<'i, R>>>(&mut self, pairs: P) -> T {
+        self.expr(&mut pairs.peekable(), 0)
+    }
+
+    fn expr<P: Iterator<Item = Pair<'i, R>>>(&mut self, pairs: &mut Peekable<P>, rbp: Prec) -> T {
+        let mut lhs = self.nud(pairs);
+        while rbp < self.lbp(pairs) {
+            lhs = self.led(pairs, lhs);
+        }
+        lhs
+    }
+
+    /// Null-Denotation
+    ///
+    /// "the action that should happen when the symbol is encountered
+    ///  as start of an expression (most notably, prefix operators)
+    fn nud<P: Iterator<Item = Pair<'i, R>>>(&mut self, pairs: &mut Peekable<P>) -> T {
+        let pair = pairs.next().expect("Pratt parsing expects non-empty Pairs");
+        match self.pratt.ops.get(&pair.as_rule()) {
+            Some((Affix::Prefix, prec)) => {
+                let rhs = self.expr(pairs, *prec - 1);
+                match self.prefix.as_mut() {
+                    Some(prefix) => prefix(pair, rhs),
+                    None => panic!("Could not map {}, no `.map_prefix(...)` specified", pair),
+                }
+            }
+            None => (self.primary)(pair),
+            _ => panic!("Expected prefix or primary expression, found {}", pair),
+        }
+    }
+
+    /// Left-Denotation
+    ///
+    /// "the action that should happen when the symbol is encountered
+    /// after the start of an expression (most notably, infix and postfix operators)"
+    fn led<P: Iterator<Item = Pair<'i, R>>>(&mut self, pairs: &mut Peekable<P>, lhs: T) -> T {
+        let pair = pairs.next().unwrap();
+        match self.pratt.ops.get(&pair.as_rule()) {
+            Some((Affix::Infix(assoc), prec)) => {
+                let rhs = match *assoc {
+                    Assoc::Left => self.expr(pairs, *prec),
+                    Assoc::Right => self.expr(pairs, *prec - 1),
+                };
+                match self.infix.as_mut() {
+                    Some(infix) => infix(lhs, pair, rhs),
+                    None => panic!("Could not map {}, no `.map_infix(...)` specified", pair),
+                }
+            }
+            Some((Affix::Postfix, _)) => match self.postfix.as_mut() {
+                Some(postfix) => postfix(lhs, pair),
+                None => panic!("Could not map {}, no `.map_postfix(...)` specified", pair),
+            },
+            _ => panic!("Expected postfix or infix expression, found {}", pair),
+        }
+    }
+
+    /// Left-Binding-Power
+    ///
+    /// "describes the symbol's precedence in infix form (most notably, operator precedence)"
+    fn lbp<P: Iterator<Item = Pair<'i, R>>>(&mut self, pairs: &mut Peekable<P>) -> Prec {
+        match pairs.peek() {
+            Some(pair) => match self.pratt.ops.get(&pair.as_rule()) {
+                Some((_, prec)) => *prec,
+                None => panic!("Expected operator, found {}", pair),
+            },
+            None => 0,
+        }
+    }
+}
diff --git a/vm/Cargo.toml b/vm/Cargo.toml
index 64311a3a..122754a0 100644
--- a/vm/Cargo.toml
+++ b/vm/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "pest_vm"
 description = "pest grammar virtual machine"
-version = "2.3.1"
+version = "2.4.0"
 edition = "2018"
 authors = ["Dragoș Tiselice <dragostiselice@gmail.com>"]
 homepage = "https://pest-parser.github.io/"
@@ -14,5 +14,5 @@ readme = "_README.md"
 rust-version = "1.56"
 
 [dependencies]
-pest = { path = "../pest", version = "2.3.1" }
-pest_meta = { path = "../meta", version = "2.3.1" }
+pest = { path = "../pest", version = "2.4.0" }
+pest_meta = { path = "../meta", version = "2.4.0" }


Test patch:
diff --git a/pest/tests/calculator.rs b/pest/tests/calculator.rs
index 54a50cc6..7935877a 100644
--- a/pest/tests/calculator.rs
+++ b/pest/tests/calculator.rs
@@ -12,7 +12,7 @@ extern crate pest;
 
 use pest::error::Error;
 use pest::iterators::{Pair, Pairs};
-use pest::prec_climber::{Assoc, Operator, PrecClimber};
+use pest::pratt_parser::{Assoc, Op, PrattParser};
 use pest::{state, ParseResult, Parser, ParserState};
 
 #[allow(dead_code, non_camel_case_types)]
@@ -109,8 +109,14 @@ impl Parser<Rule> for CalculatorParser {
     }
 }
 
-fn consume(pair: Pair<Rule>, climber: &PrecClimber<Rule>) -> i32 {
-    let primary = |pair| consume(pair, climber);
+#[allow(deprecated)]
+enum PrattOrPrecClimber<'a> {
+    Pratt(&'a PrattParser<Rule>),
+    PrecClimber(&'a pest::prec_climber::PrecClimber<Rule>),
+}
+
+fn consume(pair: Pair<Rule>, pratt_or_climber: &PrattOrPrecClimber) -> i32 {
+    let primary = |pair| consume(pair, pratt_or_climber);
     let infix = |lhs: i32, op: Pair<Rule>, rhs: i32| match op.as_rule() {
         Rule::plus => lhs + rhs,
         Rule::minus => lhs - rhs,
@@ -121,9 +127,16 @@ fn consume(pair: Pair<Rule>, climber: &PrecClimber<Rule>) -> i32 {
         _ => unreachable!(),
     };
 
-    match pair.as_rule() {
-        Rule::expression => climber.climb(pair.into_inner(), primary, infix),
-        Rule::number => pair.as_str().parse().unwrap(),
+    #[allow(deprecated)]
+    match (pair.as_rule(), pratt_or_climber) {
+        (Rule::expression, PrattOrPrecClimber::Pratt(pratt)) => pratt
+            .map_primary(primary)
+            .map_infix(infix)
+            .parse(pair.into_inner()),
+        (Rule::expression, PrattOrPrecClimber::PrecClimber(climber)) => {
+            climber.climb(pair.into_inner(), primary, infix)
+        }
+        (Rule::number, _) => pair.as_str().parse().unwrap(),
         _ => unreachable!(),
     }
 }
@@ -187,7 +200,9 @@ fn expression() {
 }
 
 #[test]
+#[allow(deprecated)]
 fn prec_climb() {
+    use pest::prec_climber::{Assoc, Operator, PrecClimber};
     let climber = PrecClimber::new(vec![
         Operator::new(Rule::plus, Assoc::Left) | Operator::new(Rule::minus, Assoc::Left),
         Operator::new(Rule::times, Assoc::Left)
@@ -197,5 +212,30 @@ fn prec_climb() {
     ]);
 
     let pairs = CalculatorParser::parse(Rule::expression, "-12+3*(4-9)^3^2/9%7381");
-    assert_eq!(-1_525, consume(pairs.unwrap().next().unwrap(), &climber));
+    assert_eq!(
+        -1_525,
+        consume(
+            pairs.unwrap().next().unwrap(),
+            &PrattOrPrecClimber::PrecClimber(&climber)
+        )
+    );
+}
+
+#[test]
+fn pratt_parse() {
+    let pratt = PrattParser::new()
+        .op(Op::infix(Rule::plus, Assoc::Left) | Op::infix(Rule::minus, Assoc::Left))
+        .op(Op::infix(Rule::times, Assoc::Left)
+            | Op::infix(Rule::divide, Assoc::Left)
+            | Op::infix(Rule::modulus, Assoc::Left))
+        .op(Op::infix(Rule::power, Assoc::Right));
+
+    let pairs = CalculatorParser::parse(Rule::expression, "-12+3*(4-9)^3^2/9%7381");
+    assert_eq!(
+        -1_525,
+        consume(
+            pairs.unwrap().next().unwrap(),
+            &PrattOrPrecClimber::Pratt(&pratt)
+        )
+    );
 }