Repository: kevinmehall/rust-peg (language: rust)

Issue:
Add support for left-recursive rules
Hey, I'd like to start working on adding support for left-recursive rules, which are of the form:

```
rule sum() -> Expression = sum() + number()
```

What do you guys think? Would you be interested in a PR?

The main motivation behind this is to parse the new syntax introduced by CPython 3.9/3.10. Guido [has already described an approach](https://medium.com/@gvanrossum_83706/left-recursive-peg-grammars-65dab3c580e1) that looks promising, following that would be my first go.


Discussion / hints:
Have you looked at rust-peg's existing support for [operator precedence via precedence climbing](https://docs.rs/peg/0.7.0/peg/#precedence-climbing)? Or are you hoping to be able to translate the python grammar directly?

I've only skimmed the linked post, but it looks like a better explanation or simpler version of the approach used by [OMeta](http://vpri.org/pdf/tr2007002_packrat.pdf). The drawback is that it's tied to packrat parsing / memoization, which rust-peg has some support for with `#[cache]`, but I've not found it to be a performance win in practice, and is not often enabled. I tend to write peg grammars as a shorthand for how I'd hand-code a recursive descent parser, and avoid code that needs to repeatedly re-parse the same input.

But if you can make straightforward extension to `#[cache]` (`#[cache_left_rec]`?), to enable left recursion, I'd accept that PR. The code that implements `#[cache]` is pretty gross, though...
> Or are you hoping to be able to translate the python grammar directly?

We're trying to keep as much of the original grammar intact as possible to reduce maintenance burden further down the line.

I think the extension of `cache` -> `cache_left_rec` would be the path of least resistance here for us. Thanks for this awesome crate btw!

Fix patch:
diff --git a/peg-macros/analysis.rs b/peg-macros/analysis.rs
index d89f8fc..ffdac50 100644
--- a/peg-macros/analysis.rs
+++ b/peg-macros/analysis.rs
@@ -89,21 +89,25 @@ impl<'a> LeftRecursionVisitor<'a> {
             RuleExpr(ref rule_ident, _) => {
                 let name = rule_ident.to_string();
 
-                if let Some(loop_start) = self
-                    .stack
-                    .iter()
-                    .position(|caller_name| caller_name == &name)
-                {
-                    let mut recursive_loop = self.stack[loop_start..].to_vec();
-                    recursive_loop.push(name);
-                    self.errors.push(LeftRecursionError {
-                        path: recursive_loop,
-                        span: rule_ident.span(),
-                    });
-                    return false;
-                }
-
                 if let Some(rule) = self.rules.get(&name) {
+                    if let Some(loop_start) = self
+                        .stack
+                        .iter()
+                        .position(|caller_name| caller_name == &name)
+                    {
+                        let mut recursive_loop = self.stack[loop_start..].to_vec();
+                        recursive_loop.push(name.clone());
+                        match rule.cache {
+                            None | Some(Cache::Simple) =>
+                                self.errors.push(LeftRecursionError {
+                                    path: recursive_loop,
+                                    span: rule_ident.span(),
+                                }),
+                            _ => ()
+
+                        }
+                        return false;
+                    }
                     self.walk_rule(rule)
                 } else {
                     // Missing rule would have already been reported
diff --git a/peg-macros/ast.rs b/peg-macros/ast.rs
index ae00d84..24ff565 100644
--- a/peg-macros/ast.rs
+++ b/peg-macros/ast.rs
@@ -26,6 +26,12 @@ pub enum Item {
     Rule(Rule),
 }
 
+#[derive(Debug)]
+pub enum Cache {
+    Simple,
+    Recursive
+}
+
 #[derive(Debug)]
 pub struct Rule {
     pub span: Span,
@@ -36,7 +42,7 @@ pub struct Rule {
     pub ret_type: Option<TokenStream>,
     pub doc: Option<TokenStream>,
     pub visibility: Option<TokenStream>,
-    pub cached: bool,
+    pub cache: Option<Cache>,
     pub no_eof: bool,
 }
 
diff --git a/peg-macros/grammar.rs b/peg-macros/grammar.rs
index c05c32d..dd8fb21 100644
--- a/peg-macros/grammar.rs
+++ b/peg-macros/grammar.rs
@@ -379,7 +379,7 @@ pub mod peg {
                 ::peg::RuleResult::Matched(__pos, doc) => {
                     let __seq_res = __parse_cacheflag(__input, __state, __err_state, __pos);
                     match __seq_res {
-                        ::peg::RuleResult::Matched(__pos, cached) => {
+                        ::peg::RuleResult::Matched(__pos, cache) => {
                             let __seq_res =
                                 __parse_no_eof_flag(__input, __state, __err_state, __pos);
                             match __seq_res {
@@ -445,7 +445,7 @@ pub mod peg {
                                                                     header,
                                                                 ) => {
                                                                     let __seq_res = match match :: peg :: ParseLiteral :: parse_string_literal (__input , __pos , "->") { :: peg :: RuleResult :: Matched (__pos , __val) => { { let __seq_res = { let str_start = __pos ; match match __parse_rust_type (__input , __state , __err_state , __pos) { :: peg :: RuleResult :: Matched (pos , _) => :: peg :: RuleResult :: Matched (pos , ()) , :: peg :: RuleResult :: Failed => :: peg :: RuleResult :: Failed , } { :: peg :: RuleResult :: Matched (__newpos , _) => { :: peg :: RuleResult :: Matched (__newpos , :: peg :: ParseSlice :: parse_slice (__input , str_start , __newpos)) } , :: peg :: RuleResult :: Failed => :: peg :: RuleResult :: Failed , } } ; match __seq_res { :: peg :: RuleResult :: Matched (__pos , t) => { :: peg :: RuleResult :: Matched (__pos , (|| { t }) ()) } :: peg :: RuleResult :: Failed => :: peg :: RuleResult :: Failed , } } } :: peg :: RuleResult :: Failed => { __err_state . mark_failure (__pos , "\"->\"") ; :: peg :: RuleResult :: Failed } } { :: peg :: RuleResult :: Matched (__newpos , __value) => { :: peg :: RuleResult :: Matched (__newpos , Some (__value)) } , :: peg :: RuleResult :: Failed => { :: peg :: RuleResult :: Matched (__pos , None) } , } ;
-                                                                    match __seq_res { :: peg :: RuleResult :: Matched (__pos , ret_type) => { match :: peg :: ParseLiteral :: parse_string_literal (__input , __pos , "=") { :: peg :: RuleResult :: Matched (__pos , __val) => { { let __seq_res = __parse_expression (__input , __state , __err_state , __pos) ; match __seq_res { :: peg :: RuleResult :: Matched (__pos , expr) => { { let __seq_res = match match :: peg :: ParseLiteral :: parse_string_literal (__input , __pos , ";") { :: peg :: RuleResult :: Matched (__pos , __val) => { :: peg :: RuleResult :: Matched (__pos , __val) } :: peg :: RuleResult :: Failed => { __err_state . mark_failure (__pos , "\";\"") ; :: peg :: RuleResult :: Failed } } { :: peg :: RuleResult :: Matched (__newpos , _) => { :: peg :: RuleResult :: Matched (__newpos , ()) } , :: peg :: RuleResult :: Failed => { :: peg :: RuleResult :: Matched (__pos , ()) } , } ; match __seq_res { :: peg :: RuleResult :: Matched (__pos , _) => { :: peg :: RuleResult :: Matched (__pos , (|| { Rule { span , doc , name : header . 0 , ty_params : header . 1 , params : header . 2 , expr , ret_type , visibility , no_eof , cached } }) ()) } :: peg :: RuleResult :: Failed => :: peg :: RuleResult :: Failed , } } } :: peg :: RuleResult :: Failed => :: peg :: RuleResult :: Failed , } } } :: peg :: RuleResult :: Failed => { __err_state . mark_failure (__pos , "\"=\"") ; :: peg :: RuleResult :: Failed } } } :: peg :: RuleResult :: Failed => :: peg :: RuleResult :: Failed , }
+                                                                    match __seq_res { :: peg :: RuleResult :: Matched (__pos , ret_type) => { match :: peg :: ParseLiteral :: parse_string_literal (__input , __pos , "=") { :: peg :: RuleResult :: Matched (__pos , __val) => { { let __seq_res = __parse_expression (__input , __state , __err_state , __pos) ; match __seq_res { :: peg :: RuleResult :: Matched (__pos , expr) => { { let __seq_res = match match :: peg :: ParseLiteral :: parse_string_literal (__input , __pos , ";") { :: peg :: RuleResult :: Matched (__pos , __val) => { :: peg :: RuleResult :: Matched (__pos , __val) } :: peg :: RuleResult :: Failed => { __err_state . mark_failure (__pos , "\";\"") ; :: peg :: RuleResult :: Failed } } { :: peg :: RuleResult :: Matched (__newpos , _) => { :: peg :: RuleResult :: Matched (__newpos , ()) } , :: peg :: RuleResult :: Failed => { :: peg :: RuleResult :: Matched (__pos , ()) } , } ; match __seq_res { :: peg :: RuleResult :: Matched (__pos , _) => { :: peg :: RuleResult :: Matched (__pos , (|| { Rule { span , doc , name : header . 0 , ty_params : header . 1 , params : header . 2 , expr , ret_type , visibility , no_eof , cache } }) ()) } :: peg :: RuleResult :: Failed => :: peg :: RuleResult :: Failed , } } } :: peg :: RuleResult :: Failed => :: peg :: RuleResult :: Failed , } } } :: peg :: RuleResult :: Failed => { __err_state . mark_failure (__pos , "\"=\"") ; :: peg :: RuleResult :: Failed } } } :: peg :: RuleResult :: Failed => :: peg :: RuleResult :: Failed , }
                                                                 }
                                                                 ::peg::RuleResult::Failed => {
                                                                     ::peg::RuleResult::Failed
@@ -482,7 +482,7 @@ pub mod peg {
         __state: &mut ParseState<'input>,
         __err_state: &mut ::peg::error::ErrorState,
         __pos: usize,
-    ) -> ::peg::RuleResult<bool> {
+    ) -> ::peg::RuleResult<Option<Cache>> {
         #![allow(non_snake_case, unused, clippy::redundant_closure_call)]
         {
             let __choice_res = match ::peg::ParseLiteral::parse_string_literal(__input, __pos, "#")
@@ -497,7 +497,10 @@ pub mod peg {
                                         __input, __pos, "]",
                                     ) {
                                         ::peg::RuleResult::Matched(__pos, __val) => {
-                                            ::peg::RuleResult::Matched(__pos, (|| true)())
+                                            ::peg::RuleResult::Matched(
+                                                __pos,
+                                                (|| Some(Cache::Simple))(),
+                                            )
                                         }
                                         ::peg::RuleResult::Failed => {
                                             __err_state.mark_failure(__pos, "\"]\"");
@@ -526,7 +529,59 @@ pub mod peg {
                 ::peg::RuleResult::Matched(__pos, __value) => {
                     ::peg::RuleResult::Matched(__pos, __value)
                 }
-                ::peg::RuleResult::Failed => ::peg::RuleResult::Matched(__pos, (|| false)()),
+                ::peg::RuleResult::Failed => {
+                    let __choice_res =
+                        match ::peg::ParseLiteral::parse_string_literal(__input, __pos, "#") {
+                            ::peg::RuleResult::Matched(__pos, __val) => {
+                                match ::peg::ParseLiteral::parse_string_literal(__input, __pos, "[")
+                                {
+                                    ::peg::RuleResult::Matched(__pos, __val) => {
+                                        match ::peg::ParseLiteral::parse_string_literal(
+                                            __input,
+                                            __pos,
+                                            "cache_left_rec",
+                                        ) {
+                                            ::peg::RuleResult::Matched(__pos, __val) => {
+                                                match ::peg::ParseLiteral::parse_string_literal(
+                                                    __input, __pos, "]",
+                                                ) {
+                                                    ::peg::RuleResult::Matched(__pos, __val) => {
+                                                        ::peg::RuleResult::Matched(
+                                                            __pos,
+                                                            (|| Some(Cache::Recursive))(),
+                                                        )
+                                                    }
+                                                    ::peg::RuleResult::Failed => {
+                                                        __err_state.mark_failure(__pos, "\"]\"");
+                                                        ::peg::RuleResult::Failed
+                                                    }
+                                                }
+                                            }
+                                            ::peg::RuleResult::Failed => {
+                                                __err_state
+                                                    .mark_failure(__pos, "\"cache_left_rec\"");
+                                                ::peg::RuleResult::Failed
+                                            }
+                                        }
+                                    }
+                                    ::peg::RuleResult::Failed => {
+                                        __err_state.mark_failure(__pos, "\"[\"");
+                                        ::peg::RuleResult::Failed
+                                    }
+                                }
+                            }
+                            ::peg::RuleResult::Failed => {
+                                __err_state.mark_failure(__pos, "\"#\"");
+                                ::peg::RuleResult::Failed
+                            }
+                        };
+                    match __choice_res {
+                        ::peg::RuleResult::Matched(__pos, __value) => {
+                            ::peg::RuleResult::Matched(__pos, __value)
+                        }
+                        ::peg::RuleResult::Failed => ::peg::RuleResult::Matched(__pos, (|| None)()),
+                    }
+                }
             }
         }
     }
diff --git a/peg-macros/grammar.rustpeg b/peg-macros/grammar.rustpeg
index cd6c353..c7cfa5f 100644
--- a/peg-macros/grammar.rustpeg
+++ b/peg-macros/grammar.rustpeg
@@ -16,7 +16,7 @@ rule grammar_args() -> Vec<(Ident, TokenStream)>
     = "(" args:((i:IDENT() ":" t:$(rust_type()) { (i, t) })**",") ","? ")" { args }
 
 rule peg_rule() -> Rule
-    = doc:rust_doc_comment() cached:cacheflag() no_eof:no_eof_flag() visibility:rust_visibility()
+    = doc:rust_doc_comment() cache:cacheflag() no_eof:no_eof_flag() visibility:rust_visibility()
       span:sp() "rule"
       header:(
           &("_" / "__" / "___") name:IDENT() ("(" ")")? { (name, None, Vec::new()) }
@@ -24,9 +24,9 @@ rule peg_rule() -> Rule
       )
       ret_type:("->" t:$(rust_type()) {t})?
       "=" expr:expression() ";"?
-        { Rule { span, doc, name:header.0, ty_params:header.1, params:header.2, expr, ret_type, visibility, no_eof, cached } }
+        { Rule { span, doc, name:header.0, ty_params:header.1, params:header.2, expr, ret_type, visibility, no_eof, cache } }
 
-    rule cacheflag() -> bool = "#" "[" "cache" "]" {true} / {false}
+    rule cacheflag() -> Option<Cache> = "#" "[" "cache" "]" {Some(Cache::Simple)} / "#" "[" "cache_left_rec" "]" {Some(Cache::Recursive)} / {None}
 
     rule no_eof_flag() -> bool = "#" "[" "no_eof" "]" {true} / {false}
 
diff --git a/peg-macros/translate.rs b/peg-macros/translate.rs
index 8a9a75c..6c5471e 100644
--- a/peg-macros/translate.rs
+++ b/peg-macros/translate.rs
@@ -81,10 +81,10 @@ pub(crate) fn compile_grammar(grammar: &Grammar) -> TokenStream {
             Item::Use(tt) => items.push(tt.clone()),
             Item::Rule(rule) => {
                 if seen_rule_names.insert(rule.name.to_string()) {
-                    if rule.cached && !(rule.params.is_empty() && rule.ty_params.is_none()) {
+                    if rule.cache.is_some() && !(rule.params.is_empty() && rule.ty_params.is_none()) {
                         items.push(report_error(
                             rule.name.span(),
-                            format!("rules with generics or parameters cannot use #[cache]"),
+                            format!("rules with generics or parameters cannot use #[cache] or #[cache_left_rec]"),
                         ));
                         continue;
                     }
@@ -153,7 +153,7 @@ fn make_parse_state(grammar: &Grammar) -> TokenStream {
     let mut cache_fields_def: Vec<TokenStream> = Vec::new();
     let mut cache_fields: Vec<Ident> = Vec::new();
     for rule in grammar.iter_rules() {
-        if rule.cached && rule.params.is_empty() && rule.ty_params.is_none() {
+        if rule.cache.is_some() && rule.params.is_empty() && rule.ty_params.is_none() {
             let name = format_ident!("{}_cache", rule.name);
             let ret_ty = rule.ret_type.clone().unwrap_or_else(|| quote!(()));
             cache_fields_def.push(
@@ -244,39 +244,75 @@ fn compile_rule(context: &Context, rule: &Rule) -> TokenStream {
 
     let rule_params = rule_params_list(&context, rule);
 
-    let fn_body = if rule.cached {
-        let cache_field = format_ident!("{}_cache", rule.name);
-
-        let cache_trace = if cfg!(feature = "trace") {
-            let str_rule_name = rule.name.to_string();
+    match &rule.cache {
+        None =>
             quote_spanned! { span =>
-                let loc = ::peg::Parse::position_repr(__input, __pos);
-                match &entry {
-                    &::peg::RuleResult::Matched(..) => println!("[PEG_TRACE] Cached match of rule {} at {}", #str_rule_name, loc),
-                    &Failed => println!("[PEG_TRACE] Cached fail of rule {} at {}", #str_rule_name, loc),
-                };
-            }
-        } else {
-            quote!()
-        };
-
-        quote_spanned! { span =>
-            if let Some(entry) = __state.#cache_field.get(&__pos) {
-                #cache_trace
-                return entry.clone();
-            }
-            let __rule_result = #wrapped_body;
-            __state.#cache_field.insert(__pos, __rule_result.clone());
-            __rule_result
-        }
-    } else {
-        wrapped_body
-    };
+                fn #name<'input #(, #grammar_lifetime_params)* #(, #ty_params)*>(__input: #input_ty, __state: #parse_state_ty, __err_state: &mut ::peg::error::ErrorState, __pos: usize #extra_args_def #(, #rule_params)*) -> ::peg::RuleResult<#ret_ty> {
+                    #![allow(non_snake_case, unused, clippy::redundant_closure_call)]
+                    #wrapped_body
+                }
+            },
+        Some(cache_type) => {
+            let cache_field = format_ident!("{}_cache", rule.name);
+
+            let cache_trace = if cfg!(feature = "trace") {
+                let str_rule_name = rule.name.to_string();
+                quote_spanned! { span =>
+                    let loc = ::peg::Parse::position_repr(__input, __pos);
+                    match &entry {
+                        &::peg::RuleResult::Matched(..) => println!("[PEG_TRACE] Cached match of rule {} at {}", #str_rule_name, loc),
+                        &Failed => println!("[PEG_TRACE] Cached fail of rule {} at {}", #str_rule_name, loc),
+                    };
+                }
+            } else {
+                quote!()
+            };
 
-    quote_spanned! { span =>
-        fn #name<'input #(, #grammar_lifetime_params)* #(, #ty_params)*>(__input: #input_ty, __state: #parse_state_ty, __err_state: &mut ::peg::error::ErrorState, __pos: usize #extra_args_def #(, #rule_params)*) -> ::peg::RuleResult<#ret_ty> {
-            #![allow(non_snake_case, unused, clippy::redundant_closure_call)]
-            #fn_body
+            match cache_type {
+                Cache::Simple =>
+                    quote_spanned! { span =>
+                        fn #name<'input #(, #grammar_lifetime_params)* #(, #ty_params)*>(__input: #input_ty, __state: #parse_state_ty, __err_state: &mut ::peg::error::ErrorState, __pos: usize #extra_args_def #(, #rule_params)*) -> ::peg::RuleResult<#ret_ty> {
+                            #![allow(non_snake_case, unused, clippy::redundant_closure_call)]
+                            if let Some(entry) = __state.#cache_field.get(&__pos) {
+                                #cache_trace
+                                return entry.clone();
+                            }
+
+                            let __rule_result = #wrapped_body;
+                            __state.#cache_field.insert(__pos, __rule_result.clone());
+                            __rule_result
+                        }
+                    },
+                Cache::Recursive =>
+                    quote_spanned! { span =>
+                        fn #name<'input #(, #grammar_lifetime_params)* #(, #ty_params)*>(__input: #input_ty, __state: #parse_state_ty, __err_state: &mut ::peg::error::ErrorState, __pos: usize #extra_args_def #(, #rule_params)*) -> ::peg::RuleResult<#ret_ty> {
+                            #![allow(non_snake_case, unused, clippy::redundant_closure_call)]
+                            if let Some(entry) = __state.#cache_field.get(&__pos) {
+                                #cache_trace
+                                return entry.clone();
+                            }
+
+                            __state.#cache_field.insert(__pos, ::peg::RuleResult::Failed);
+                            let mut __last_result = ::peg::RuleResult::Failed;
+                            loop {
+                                let __current_result = { #wrapped_body };
+                                match __current_result {
+                                    ::peg::RuleResult::Failed => break,
+                                    ::peg::RuleResult::Matched(__current_endpos, _) =>
+                                        match __last_result {
+                                            ::peg::RuleResult::Matched(__last_endpos, _) if __current_endpos <= __last_endpos => break,
+                                            _ => {
+                                                __state.#cache_field.insert(__pos, __current_result.clone());
+                                                __last_result = __current_result;
+                                            },
+                                        }
+                                }
+                            }
+
+                            return __last_result;
+                        }
+                    }
+            }
         }
     }
 }


Test patch:
diff --git a/tests/compile-fail/cache_with_args.stderr b/tests/compile-fail/cache_with_args.stderr
index a67b1af..1caa07e 100644
--- a/tests/compile-fail/cache_with_args.stderr
+++ b/tests/compile-fail/cache_with_args.stderr
@@ -1,10 +1,10 @@
-error: rules with generics or parameters cannot use #[cache]
+error: rules with generics or parameters cannot use #[cache] or #[cache_left_rec]
  --> $DIR/cache_with_args.rs:5:10
   |
 5 |     rule foo(x: u32) = "foo" //~ ERROR
   |          ^^^
 
-error: rules with generics or parameters cannot use #[cache]
+error: rules with generics or parameters cannot use #[cache] or #[cache_left_rec]
  --> $DIR/cache_with_args.rs:8:10
   |
 8 |     rule ltarg<'a>() -> &'a str = { "" } //~ ERROR
diff --git a/tests/compile-fail/left_recursion.rs b/tests/compile-fail/left_recursion_without_cache.rs
similarity index 100%
rename from tests/compile-fail/left_recursion.rs
rename to tests/compile-fail/left_recursion_without_cache.rs
diff --git a/tests/compile-fail/left_recursion.stderr b/tests/compile-fail/left_recursion_without_cache.stderr
similarity index 78%
rename from tests/compile-fail/left_recursion.stderr
rename to tests/compile-fail/left_recursion_without_cache.stderr
index 4ffd793..69fa0b8 100644
--- a/tests/compile-fail/left_recursion.stderr
+++ b/tests/compile-fail/left_recursion_without_cache.stderr
@@ -1,23 +1,23 @@
 error: left recursive rules create an infinite loop: rec -> rec
- --> $DIR/left_recursion.rs:4:18
+ --> $DIR/left_recursion_without_cache.rs:4:18
   |
 4 |     rule rec() = rec() //~ ERROR left recursive rules create an infinite loop: rec -> rec
   |                  ^^^
 
 error: left recursive rules create an infinite loop: foo -> bar -> foo
-  --> $DIR/left_recursion.rs:12:11
+  --> $DIR/left_recursion_without_cache.rs:12:11
    |
 12 |         / foo() //~ ERROR left recursive rules create an infinite loop: foo -> bar -> foo
    |           ^^^
 
 error: left recursive rules create an infinite loop: bar -> foo -> bar
- --> $DIR/left_recursion.rs:8:11
+ --> $DIR/left_recursion_without_cache.rs:8:11
   |
 8 |         / bar() //~ ERROR left recursive rules create an infinite loop: bar -> foo -> bar
   |           ^^^
 
 error: left recursive rules create an infinite loop: prec -> prec
-  --> $DIR/left_recursion.rs:15:9
+  --> $DIR/left_recursion_without_cache.rs:15:9
    |
 15 |         prec() { () } //~ ERROR
    |         ^^^^
diff --git a/tests/run-pass/arithmetic_with_left_recursion.rs b/tests/run-pass/arithmetic_with_left_recursion.rs
new file mode 100644
index 0000000..f22c1f8
--- /dev/null
+++ b/tests/run-pass/arithmetic_with_left_recursion.rs
@@ -0,0 +1,20 @@
+extern crate peg;
+
+use arithmetic::sum;
+
+peg::parser!( grammar arithmetic() for str {
+    #[cache_left_rec]
+    pub rule sum() -> i64
+        = l:sum() "+" r:number() { l+r }
+        / number()
+
+    rule number() -> i64
+        = n:$(['0'..='9']+) { n.parse().unwrap() }
+});
+
+fn main() {
+    assert_eq!(sum("1"), Ok(1));
+    assert_eq!(sum("1+1"), Ok(2));
+    assert_eq!(sum("1+1+1"), Ok(3));
+    assert_eq!(sum("1+2+3"), Ok(6));
+}
diff --git a/tests/run-pass/grammar_with_args_and_cache.rs b/tests/run-pass/grammar_with_args_and_cache.rs
new file mode 100644
index 0000000..a2fdf4e
--- /dev/null
+++ b/tests/run-pass/grammar_with_args_and_cache.rs
@@ -0,0 +1,11 @@
+extern crate peg;
+
+peg::parser! {
+    grammar lol(config: bool) for str {
+        #[cache_left_rec]
+        rule one() -> ()
+            = one() / "foo"
+    }
+}
+
+fn main() {}