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

Issue:
Handling indentation
Hi! I don’t know if this is a question or a feature request, but I want to parse [reStructuredText](http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html). It allows nesting blocks, and [indentation works in a special way](http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#indentation):

> Several constructs begin with a marker, and the body of the construct must be indented relative to the marker. For constructs using simple markers (bullet lists, enumerated lists, footnotes, citations, hyperlink targets, directives, and comments), the level of indentation of the body is determined by the position of the first line of text, which begins on the same line as the marker. For example, bullet list bodies must be indented by at least two columns relative to the left edge of the bullet:
> 
> ```rst
> - This is the first line of a bullet list
>   item's paragraph.  All lines must align
>   relative to the first line.  [1]_
> 
>       This indented paragraph is interpreted
>       as a block quote.
> 
> Because it is not sufficiently indented,
> this paragraph does not belong to the list
> item.
> 
> .. [1] Here's a footnote.  The second line is aligned
>    with the beginning of the footnote label.  The ".."
>    marker is what determines the indentation.
> ```
> 
> For constructs using complex markers (field lists and option lists), where the marker may contain arbitrary text, the indentation of the first line after the marker determines the left edge of the body. For example, field lists may have very long markers (containing the field names):
> 
> ```rst
> :Hello: This field has a short field name, so aligning the field
>         body with the first line is feasible.
> 
> :Number-of-African-swallows-required-to-carry-a-coconut: It would
>     be very difficult to align the field body with the left edge
>     of the first line.  It may even be preferable not to begin the
>     body on the same line as the marker.
> ```

How can I parse this? Would I parse it manually, I’d define an “indent stack” (like `[2, 4]` for the first example). Is the best idea to create an iterator that handles this indent stack and only feeds the blocks it found into `pest`? Or is there another way?


Discussion / hints:
#328 should be helpful here. Let me know if you need extra help and I can take a closer look!
Not exactly, as indentation isn’t defined by just the spaces that can be pushed on the stack (as mentioned in the text). The markers also count.

Furthermore, I’d need a way to say “if the stack is empty, match "".” AFAIK, `PEEK` fails to match on an empty stack. I *assume* I’d need the following extensions

- `PEEK` → `PEEK` (as before) or `PEEK(default)` (matches “default” if the stack is empty)
- `PUSH` → `PUSH(expr)` (as before) or `PUSH(expr, lambda)` (executes the lambda on the captured range and pushes the return value)

and then I could do:

```peg
block = { PEEK("") ~ (bullet_list | …) }
bullet_list = { (bullet_list_item | empty_line)+ }
bullet_list_item = { PUSH("-" ~ " "+, m => m.replace('-', " ")) ~ line ~ block* ~ POP }
line = { !bullet_list_item ~ ANY* }
```
Markers can probably be dealt with by having multiple choices:

```
block = { "-" ~ bullet_list | "^" ~ whatever }
bullet_list = { ... }
whatever = { ... }
```

Why do you need to know when the stack is empty?
I see the issue with the pushes, though. You'd need a way to simply figure out the distance from the start of the line. I'm wondering what the smallest primitive is in order to enable this.
> You'd need a way to simply figure out the distance from the start of the line.

This is problem 1:

If I want to populate the stack with the indentation, I need to be able to push `" "{x}` onto the stack, while matching `something{y} ~ " "{x-y}` (e.g. I match `"-" ~ " "{2}` and push `" "{3}`)

Of course if there would be another way than the stack, that would work too, but having a stack is the most intuitive.

> Why do you need to know when the stack is empty?

This is problem 2:

Provided the stack contains the indentations of the first example, i.e. `["  ", "    "]`, I’d match the indentation via `PEEK ~ line`. But that should also work on an empty stack!

I could just do `document = { SOI ~ PUSH("") ~ block* ~ EOI }`, but then I can’t test rules right? `RstParser::parse(Rule::block, ...)` wouldn’t match, since `parse` initializes the stack empty and `PEEK` will not match an empty stack.
I've managed to get [something](https://pest-parser.github.io/?bin=14tp26#editor) working. The issue here is that the order of pushing vs the order of popping is reversed, but it works if `spaces` only contains spaces and not tabs, for example.

Hmm, that doesn’t work. All kinds of blocks can be nested (your `rest_lines?` should be something like `block*`)

I tried something, but I can’t get it to work: https://pest.rs/?bin=7wn0u#editor

I don’t think the PUSH/POP semantics allow for what I need.
I think this is possible by using `PEEK_ALL`, like in the simple lists [example](https://github.com/pest-parser/pest/blob/master/derive/tests/lists.pest). It uses `PUSH` and `DROP` in order to keep track of the indentation level.
I got it. Damn, that’s hard, but you’re right, it seems possible.

https://pest.rs/?bin=jgwd2#editor

I still didn’t get nesting to work properly (see the “nested item 2”), but if you can show me that this is possible, i’m happy. (could it be that in `blist_body`, the `POP` succeeds but the `PUSH` doesn’t? that would explain the stack being off)

also, can you please confirm that PEEK_ALL matches "" on an empty stack?
Super impressive work! 😃I really like the elegance of your example. ~~Your example is missing a `PEEK_ALL` in [the beginning of `bullet_list`](https://pest-parser.github.io/?bin=jrtvq#editor).~~ That isn't quite right. I'll have to take a better look tomorrow.

`PEEK_ALL` always matches when the stack is empty. Unfortunately, this needs to be better documented.
I don't have a complete solution, but the issue in your example is that you need to `PEEK_ALL` at every line, but `line` accepts the new line, then calls `blist_body` which should first `PEEK_ALL` before doing anything else, but doesn't.

```
bullet_item =  { "-" ~ PUSH(" "+) ~ line ~ blist_body? ~ DROP }
blist_body  = _{
  // Some PEEK_ALL is needed here
  PUSH(" " ~ POP) ~ hanging_block ~ block*
}
```

This is quite hard since, as far as I can tell, you always need to figure out if you're a paragraph or a bullet list item. Maybe you could make use of the lookahead like this:

```
  &(PEEK_ALL) ~ paragraph          // paragraph at same indentation
| &(DROP ~ "-" ~ ...) ~ blist_item // bullet list item at same parent indentation
```

Lookaheads *reset* the stack to its initial position.
I don’t think it’s possible. `blist_body` appears when the stack contains `[…, "- "]`, so i’d need `PEEK_ALL_EXCEPT_LAST ~ PUSH(" " ~ POP)`.
I guess the lookahead won't work because after you'd need to actually match it, correct?

If so, it might be worthwhile to be able to peek by slicing the stack with a range, I presume. Adding this would be no problem.
exactly. I need to match it exactly because otherwise I can’t tell the parser the depth of the next indentation level:

```
               ← stack here: []
-  item        ← stack here: ["  "] *
   next line   ← stack here: ["   "]
   - indented  ← stack here: ["   " " "] *
     next      ← stack here: ["   " "  "]
```

at the `*` positions, the last stack entry is one space too small for the following indentation, so i need to be able to both match the correct indentation and fix the last stack entry.
Should I create a separate issue for this?

and how would it look? I have two proposals using arrow slice syntax:

## 1. going from the end:

`PEEK`, `PEEK[i]`, `PEEK[skip..last]`, `PEEK_ALL`  
`skip` and `last` are optional, default to the top (0) and bottom items the stack, so `PEEK[..]` is equivalent to `PEEK_ALL`.

I propose that it fails when you specifically request an item that doesn’t exist, but succeeds when you request an empty sequence. E.g. if there’s one item on the stack (`PEEK_ALL ≡ ("stuff")`)

1. `PEEK[1]` fails
2. `PEEK[1..]` fails
3. `PEEK[..2]` fails
4. `PEEK[0] ≡ "item"` succeeds
5. `PEEK[..1] ≡ ("item")` succeeds
6. `PEEK[1..] ≡ ()` succeeds

This option would be intutive from the way stacks work: We usually think “i-th item from the top”

# 2. use negative indices

An alternative would be `PEEK`, `PEEK[i]`, `PEEK[start..end]`, `PEEK_ALL`

That would be more intuitive regarding item direction as the stack gets matched in bottom→top direction, written left-to-right. Then our examples would be:

1. `PEEK[-2]` fails
2. `PEEK[..-2]` fails
3. `PEEK[-2..]` fails
4. `PEEK[-1] ≡ "item"` succeeds
5. `PEEK[-1..] ≡ ("item")` succeeds
6. `PEEK[..-1] ≡ ()` succeeds
I've been thinking about pretty much exactly the same syntax, only having both negatives and positives. I was considering whether this should be added as a more conservative feature now in a 2.1 release or whether this should be part of a big, 3.0 pest syntax RFC that would include this change.
I feel more inclined to have this working in 2.1 without guaranteeing syntax stability post-3.0. @flying-sheep, would you like to take a jab at this? If not, I can give it a try. :)
I can try! So you mean you’d go for the second option, but allowing to do e.g. `PEEK[1..-2]` meaning “match the stack without its bottom and the two top elements”?
Yes, but you'd be able to use both negative and positive ends.
of course. it would work just like python slicing, only with `..` instead of `:`
Precisely! If there's one thing they got right in Python, it's slicing.
Oh, and, I'm here to offer guidance for this. Generally, it shouldn't be very hard to incorporate this since it's split in very granular tasks, but there might be a few caveats here and there, e.g. with validation.
So I basically start by modifying <samp>meta/grammar.pest</samp> and follow the errors until it works?
> Precisely! If there's one thing they got right in Python, it's slicing.

actually I understand why rust’s slicing doesn’t work that way:

if you could do `fn skip_last_n<T>(s: &T[], n: usize) { s[..-n] }`, it would work until someone calls `skip_last_n(0)`, because `s[..-0]` is `s[..0]` and therefore an empty slice instead of the full sequence. We’d need [Numerical Constrained Types](https://github.com/rust-lang/rfcs/issues/1621) to make this safe: `fn skip_last_n<T>(s: &T[], n: usize in 1..) { s[..-n] }`

in our case that problem doesn’t apply, because the grammars always contain literal numbers instead of variables that could be 0, this is why i suggested the negative indices.
@flying-sheep, pretty sure constrained types are still a long ways off, but here's hoping!

Modifying the grammar is a good place to start. After that, some changes need to be made to generator in order to slice the `Stack` found in pest itself.

Fix patch:
diff --git a/.gitignore b/.gitignore
index 64f40ab2..f940c081 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 target
 Cargo.lock
 .idea
+.vscode
diff --git a/derive/src/lib.rs b/derive/src/lib.rs
index 6e189b8a..24c97442 100644
--- a/derive/src/lib.rs
+++ b/derive/src/lib.rs
@@ -190,6 +190,7 @@
 //! * `POP` - pops a string from the stack and matches it
 //! * `POP_ALL` - pops the entire state of the stack and matches it
 //! * `PEEK` - peeks a string from the stack and matches it
+//! * `PEEK[a..b]` - peeks part of the stack and matches it
 //! * `PEEK_ALL` - peeks the entire state of the stack and matches it
 //! * `DROP` - drops the top of the stack (fails to match if the stack is empty)
 //!
@@ -222,8 +223,8 @@
 //! `PUSH(e)` simply pushes the captured string of the expression `e` down a stack. This stack can
 //! then later be used to match grammar based on its content with `POP` and `PEEK`.
 //!
-//! `PEEK` always matches the string at the top of stack. So, if the stack contains `["a", "b"]`,
-//! then this grammar:
+//! `PEEK` always matches the string at the top of stack. So, if the stack contains `["b", "a"]`
+//! (`"a"` being on top), this grammar:
 //!
 //! ```ignore
 //! a = { PEEK }
@@ -236,13 +237,33 @@
 //! ```
 //!
 //! `POP` works the same way with the exception that it pops the string off of the stack if the
-//! the match worked. With the stack from above, if `POP` matches `"a"`, the stack will be mutated
+//! match worked. With the stack from above, if `POP` matches `"a"`, the stack will be mutated
 //! to `["b"]`.
 //!
 //! `DROP` makes it possible to remove the string at the top of the stack
 //! without matching it. If the stack is nonempty, `DROP` drops the top of the
 //! stack. If the stack is empty, then `DROP` fails to match.
 //!
+//! ### Advanced peeking
+//!
+//! `PEEK[start..end]` and `PEEK_ALL` allow to peek deeper into the stack. The syntax works exactly
+//! like Rust’s exclusive slice syntax. Additionally, negative indices can be used to indicate an
+//! offset from the top. If the end lies before or at the start, the expression matches (as does
+//! a `PEEK_ALL` on an empty stack). With the stack `["c", "b", "a"]` (`"a"` on top):
+//!
+//! ```ignore
+//! fill = PUSH("c") ~ PUSH("b") ~ PUSH("a")
+//! v = { PEEK_ALL } = { "a" ~ "b" ~ "c" }  // top to bottom
+//! w = { PEEK[..] } = { "c" ~ "b" ~ "a" }  // bottom to top
+//! x = { PEEK[1..2] } = { PEEK[1..-1] } = { "b" }
+//! y = { PEEK[..-2] } = { PEEK[0..1] } = { "a" }
+//! z = { PEEK[1..] } = { PEEK[-2..3] } = { "c" ~ "b" }
+//! n = { PEEK[2..-2] } = { PEEK[2..1] } = { "" }
+//! ```
+//!
+//! For historical reasons, `PEEK_ALL` matches from top to bottom, while `PEEK[start..end]` matches
+//! from bottom to top. There is currectly no syntax to match a slice of the stack top to bottom.
+//!
 //! ## `Rule`
 //!
 //! All rules defined or used in the grammar populate a generated `enum` called `Rule`. This
diff --git a/generator/src/generator.rs b/generator/src/generator.rs
index 0025ec95..3be142fe 100644
--- a/generator/src/generator.rs
+++ b/generator/src/generator.rs
@@ -12,6 +12,7 @@ use std::path::Path;
 
 use proc_macro2::{Span, TokenStream};
 use syn::{self, Generics, Ident};
+use quote::{ToTokens, TokenStreamExt};
 
 use pest_meta::ast::*;
 use pest_meta::optimizer::*;
@@ -351,6 +352,12 @@ fn generate_expr(expr: OptimizedExpr) -> TokenStream {
             let ident = Ident::new(&ident, Span::call_site());
             quote! { self::#ident(state) }
         }
+        OptimizedExpr::PeekSlice(start, end_) => {
+            let end = QuoteOption(end_);
+            quote! {
+                state.stack_match_peek_slice(#start, #end, ::pest::MatchDir::BottomToTop)
+            }
+        }
         OptimizedExpr::PosPred(expr) => {
             let expr = generate_expr(*expr);
 
@@ -491,6 +498,12 @@ fn generate_expr_atomic(expr: OptimizedExpr) -> TokenStream {
             let ident = Ident::new(&ident, Span::call_site());
             quote! { self::#ident(state) }
         }
+        OptimizedExpr::PeekSlice(start, end_) => {
+            let end = QuoteOption(end_);
+            quote! {
+                state.stack_match_peek_slice(#start, #end, ::pest::MatchDir::BottomToTop)
+            }
+        }
         OptimizedExpr::PosPred(expr) => {
             let expr = generate_expr_atomic(*expr);
 
@@ -593,6 +606,17 @@ fn generate_expr_atomic(expr: OptimizedExpr) -> TokenStream {
     }
 }
 
+struct QuoteOption<T>(Option<T>);
+
+impl<T: ToTokens> ToTokens for QuoteOption<T> {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.append_all(match self.0 {
+            Some(ref t) => quote! { ::std::option::Option::Some(#t) },
+            None => quote! { ::std::option::Option::None },
+        });
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/generator/src/lib.rs b/generator/src/lib.rs
index a3abf4dc..e8c4bd77 100644
--- a/generator/src/lib.rs
+++ b/generator/src/lib.rs
@@ -65,6 +65,8 @@ pub fn derive_parser(input: TokenStream, include_grammar: bool) -> TokenStream {
                 Rule::non_atomic_modifier => "`!`".to_owned(),
                 Rule::opening_brace => "`{`".to_owned(),
                 Rule::closing_brace => "`}`".to_owned(),
+                Rule::opening_brack => "`[`".to_owned(),
+                Rule::closing_brack => "`]`".to_owned(),
                 Rule::opening_paren => "`(`".to_owned(),
                 Rule::positive_predicate_operator => "`&`".to_owned(),
                 Rule::negative_predicate_operator => "`!`".to_owned(),
diff --git a/meta/src/ast.rs b/meta/src/ast.rs
index 151b550e..4d90f773 100644
--- a/meta/src/ast.rs
+++ b/meta/src/ast.rs
@@ -33,6 +33,8 @@ pub enum Expr {
     Range(String, String),
     /// Matches the rule with the given name, e.g. `a`
     Ident(String),
+    /// Matches a custom part of the stack, e.g. `PEEK[..]`
+    PeekSlice(i32, Option<i32>),
     /// Positive lookahead; matches expression without making progress, e.g. `&e`
     PosPred(Box<Expr>),
     /// Negative lookahead; matches if expression doesn't match, without making progress, e.g. `!e`
diff --git a/meta/src/grammar.pest b/meta/src/grammar.pest
index 64826f61..0d03ba89 100644
--- a/meta/src/grammar.pest
+++ b/meta/src/grammar.pest
@@ -19,6 +19,8 @@ opening_brace       = { "{" }
 closing_brace       = { "}" }
 opening_paren       = { "(" }
 closing_paren       = { ")" }
+opening_brack       = { "[" }
+closing_brack       = { "]" }
 
 modifier = _{
     silent_modifier |
@@ -35,7 +37,7 @@ non_atomic_modifier      = { "!" }
 expression =  { term ~ (infix_operator ~ term)* }
 term       =  { prefix_operator* ~ node ~ postfix_operator* }
 node       = _{ opening_paren ~ expression ~ closing_paren | terminal }
-terminal   = _{ _push | identifier | string | insensitive_string | range }
+terminal   = _{ _push | peek_slice | identifier | string | insensitive_string | range }
 
 prefix_operator  = _{ positive_predicate_operator | negative_predicate_operator }
 infix_operator   = _{ sequence_operator | choice_operator }
@@ -63,10 +65,12 @@ repeat_max     = { opening_brace ~ comma ~ number ~ closing_brace }
 repeat_min_max = { opening_brace ~ number ~ comma ~ number ~ closing_brace }
 
 number = @{ '0'..'9'+ }
+integer = @{ number | "-" ~ "0"* ~ '1'..'9' ~ number? }
 
 comma = { "," }
 
 _push = { "PUSH" ~ opening_paren ~ expression ~ closing_paren }
+peek_slice = { "PEEK" ~ opening_brack ~ integer? ~ range_operator ~ integer? ~ closing_brack }
 
 identifier = @{ !"PUSH" ~ ("_" | alpha) ~ ("_" | alpha_num)* }
 alpha      = _{ 'a'..'z' | 'A'..'Z' }
diff --git a/meta/src/optimizer/mod.rs b/meta/src/optimizer/mod.rs
index 5c3bc027..89bd0721 100644
--- a/meta/src/optimizer/mod.rs
+++ b/meta/src/optimizer/mod.rs
@@ -42,6 +42,7 @@ fn rule_to_optimized_rule(rule: Rule) -> OptimizedRule {
             Expr::Insens(string) => OptimizedExpr::Insens(string),
             Expr::Range(start, end) => OptimizedExpr::Range(start, end),
             Expr::Ident(ident) => OptimizedExpr::Ident(ident),
+            Expr::PeekSlice(start, end) => OptimizedExpr::PeekSlice(start, end),
             Expr::PosPred(expr) => OptimizedExpr::PosPred(Box::new(to_optimized(*expr))),
             Expr::NegPred(expr) => OptimizedExpr::NegPred(Box::new(to_optimized(*expr))),
             Expr::Seq(lhs, rhs) => {
@@ -54,7 +55,9 @@ fn rule_to_optimized_rule(rule: Rule) -> OptimizedRule {
             Expr::Rep(expr) => OptimizedExpr::Rep(Box::new(to_optimized(*expr))),
             Expr::Skip(strings) => OptimizedExpr::Skip(strings),
             Expr::Push(expr) => OptimizedExpr::Push(Box::new(to_optimized(*expr))),
-            _ => unreachable!("No valid transformation to OptimizedRule")
+            Expr::RepOnce(_) | Expr::RepExact(..) | Expr::RepMin(..) | Expr::RepMax(..) | Expr::RepMinMax(..) => {
+                unreachable!("No valid transformation to OptimizedRule")
+            }
         }
     }
 
@@ -82,6 +85,7 @@ pub enum OptimizedExpr {
     Insens(String),
     Range(String, String),
     Ident(String),
+    PeekSlice(i32, Option<i32>),
     PosPred(Box<OptimizedExpr>),
     NegPred(Box<OptimizedExpr>),
     Seq(Box<OptimizedExpr>, Box<OptimizedExpr>),
diff --git a/meta/src/parser.rs b/meta/src/parser.rs
index 98ab0830..11f59b5f 100644
--- a/meta/src/parser.rs
+++ b/meta/src/parser.rs
@@ -109,6 +109,7 @@ pub enum ParserExpr<'i> {
     Insens(String),
     Range(String, String),
     Ident(String),
+    PeekSlice(i32, Option<i32>),
     PosPred(Box<ParserNode<'i>>),
     NegPred(Box<ParserNode<'i>>),
     Seq(Box<ParserNode<'i>>, Box<ParserNode<'i>>),
@@ -139,6 +140,7 @@ fn convert_node(node: ParserNode) -> Expr {
         ParserExpr::Insens(string) => Expr::Insens(string),
         ParserExpr::Range(start, end) => Expr::Range(start, end),
         ParserExpr::Ident(ident) => Expr::Ident(ident),
+        ParserExpr::PeekSlice(start, end) => Expr::PeekSlice(start, end),
         ParserExpr::PosPred(node) => Expr::PosPred(Box::new(convert_node(*node))),
         ParserExpr::NegPred(node) => Expr::NegPred(Box::new(convert_node(*node))),
         ParserExpr::Seq(node1, node2) => Expr::Seq(
@@ -271,6 +273,32 @@ fn consume_expr<'i>(
                             span: start.span(&end)
                         }
                     }
+                    Rule::peek_slice => {
+                        let mut pairs = pair.clone().into_inner();
+                        pairs.next().unwrap(); // opening_brack
+                        let pair_start = pairs.next().unwrap(); // .. or integer
+                        let start: i32 = match pair_start.as_rule() {
+                            Rule::range_operator => 0,
+                            Rule::integer => {
+                                pairs.next().unwrap(); // ..
+                                pair_start.as_str().parse().unwrap()
+                            },
+                            _ => unreachable!()
+                        };
+                        let pair_end = pairs.next().unwrap(); // integer or }
+                        let end: Option<i32> = match pair_end.as_rule() {
+                            Rule::closing_brack => None,
+                            Rule::integer => {
+                                pairs.next().unwrap(); // }
+                                Some(pair_end.as_str().parse().unwrap())
+                            },
+                            _ => unreachable!()
+                        };
+                        ParserNode {
+                            expr: ParserExpr::PeekSlice(start, end),
+                            span: pair.into_span()
+                        }
+                    },
                     Rule::identifier => ParserNode {
                         expr: ParserExpr::Ident(pair.as_str().to_owned()),
                         span: pair.clone().into_span()
@@ -800,6 +828,74 @@ mod tests {
         };
     }
 
+    #[test]
+    fn peek_slice_all() {
+        parses_to! {
+            parser: PestParser,
+            input: "PEEK[..]",
+            rule: Rule::peek_slice,
+            tokens: [
+                peek_slice(0, 8, [
+                    opening_brack(4, 5),
+                    range_operator(5, 7),
+                    closing_brack(7, 8)
+                ])
+            ]
+        };
+    }
+
+    #[test]
+    fn peek_slice_start() {
+        parses_to! {
+            parser: PestParser,
+            input: "PEEK[1..]",
+            rule: Rule::peek_slice,
+            tokens: [
+                peek_slice(0, 9, [
+                    opening_brack(4, 5),
+                    integer(5, 6),
+                    range_operator(6, 8),
+                    closing_brack(8, 9)
+                ])
+            ]
+        };
+    }
+
+    #[test]
+    fn peek_slice_end() {
+        parses_to! {
+            parser: PestParser,
+            input: "PEEK[ ..-1]",
+            rule: Rule::peek_slice,
+            tokens: [
+                peek_slice(0, 11, [
+                    opening_brack(4, 5),
+                    range_operator(6, 8),
+                    integer(8, 10),
+                    closing_brack(10, 11)
+                ])
+            ]
+        };
+    }
+
+    #[test]
+    fn peek_slice_start_end() {
+        parses_to! {
+            parser: PestParser,
+            input: "PEEK[-5..10]",
+            rule: Rule::peek_slice,
+            tokens: [
+                peek_slice(0, 12, [
+                    opening_brack(4, 5),
+                    integer(5, 7),
+                    range_operator(7, 9),
+                    integer(9, 11),
+                    closing_brack(11, 12)
+                ])
+            ]
+        };
+    }
+
     #[test]
     fn identifier() {
         parses_to! {
@@ -1047,6 +1143,7 @@ mod tests {
                 Rule::positive_predicate_operator,
                 Rule::negative_predicate_operator,
                 Rule::_push,
+                Rule::peek_slice,
                 Rule::identifier,
                 Rule::insensitive_string,
                 Rule::quote,
@@ -1168,6 +1265,29 @@ mod tests {
             ]
         );
     }
+    
+    #[test]
+    fn ast_peek_slice() {
+        let input = "rule = _{ PEEK[-04..] ~ PEEK[..3] }";
+
+        let pairs = PestParser::parse(Rule::grammar_rules, input).unwrap();
+        let ast = consume_rules_with_spans(pairs).unwrap();
+        let ast: Vec<_> = ast.into_iter().map(|rule| convert_rule(rule)).collect();
+
+        assert_eq!(
+            ast,
+            vec![
+                AstRule {
+                    name: "rule".to_owned(),
+                    ty: RuleType::Silent,
+                    expr: Expr::Seq(
+                        Box::new(Expr::PeekSlice(-4, None)),
+                        Box::new(Expr::PeekSlice(0, Some(3))),
+                    )
+                }
+            ],
+        );
+    }
 
     #[test]
     #[should_panic(expected = "grammar error
diff --git a/pest/src/lib.rs b/pest/src/lib.rs
index fea3a6e7..ac2268fb 100644
--- a/pest/src/lib.rs
+++ b/pest/src/lib.rs
@@ -62,7 +62,7 @@
 extern crate ucd_trie;
 
 pub use parser::Parser;
-pub use parser_state::{state, Atomicity, Lookahead, ParseResult, ParserState};
+pub use parser_state::{state, Atomicity, Lookahead, ParseResult, ParserState, MatchDir};
 pub use position::Position;
 pub use span::Span;
 use std::fmt::Debug;
diff --git a/pest/src/parser_state.rs b/pest/src/parser_state.rs
index 4bfe0a89..96f87c80 100644
--- a/pest/src/parser_state.rs
+++ b/pest/src/parser_state.rs
@@ -40,6 +40,13 @@ pub enum Atomicity {
 /// Type alias to simplify specifying the return value of chained closures.
 pub type ParseResult<S> = Result<S, S>;
 
+/// Match direction for the stack. Used in `PEEK[a..b]`/`stack_match_peek_slice`.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum MatchDir {
+    BottomToTop,
+    TopToBottom
+}
+
 /// The complete state of a [`Parser`].
 ///
 /// [`Parser`]: trait.Parser.html
@@ -904,31 +911,48 @@ impl<'i, R: RuleType> ParserState<'i, R> {
         self.match_string(string)
     }
 
-    /// Matches the full state of the stack.
+    /// Matches part of the state of the stack.
     ///
     /// # Examples
     ///
     /// ```
-    /// # use pest;
+    /// # use pest::{self, MatchDir};
     /// # #[allow(non_camel_case_types)]
     /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     /// enum Rule {}
     ///
-    /// let input = "aaaa";
+    /// let input = "abcd cd cb";
     /// let mut state: Box<pest::ParserState<Rule>> = pest::ParserState::new(input);
-    /// let mut result = state.stack_push(|state| state.match_string("a")).and_then(|state| {
-    ///     state.stack_push(|state| state.match_string("a"))
-    /// }).and_then(|state| state.stack_match_peek());
+    /// let mut result = state
+    ///     .stack_push(|state| state.match_string("a"))
+    ///     .and_then(|state| state.stack_push(|state| state.match_string("b")))
+    ///     .and_then(|state| state.stack_push(|state| state.match_string("c")))
+    ///     .and_then(|state| state.stack_push(|state| state.match_string("d")))
+    ///     .and_then(|state| state.match_string(" "))
+    ///     .and_then(|state| state.stack_match_peek_slice(2, None, MatchDir::BottomToTop))
+    ///     .and_then(|state| state.match_string(" "))
+    ///     .and_then(|state| state.stack_match_peek_slice(1, Some(-1), MatchDir::TopToBottom));
     /// assert!(result.is_ok());
-    /// assert_eq!(result.unwrap().position().pos(), 4);
+    /// assert_eq!(result.unwrap().position().pos(), 10);
     /// ```
     #[inline]
-    pub fn stack_match_peek(mut self: Box<Self>) -> ParseResult<Box<Self>> {
+    pub fn stack_match_peek_slice(mut self: Box<Self>, start: i32, end: Option<i32>, match_dir: MatchDir) -> ParseResult<Box<Self>> {
+        let range = match constrain_idxs(start, end, self.stack.len()) {
+            Some(r) => r,
+            None => return Err(self),
+        };
+        // return true if an empty sequence is requested
+        if range.end <= range.start { return Ok(self) }
+        
         let mut position = self.position.clone();
-        let result = self.stack.iter().all(|span| {
-            position.match_string(span.as_str())
-        });
-
+        let result = {
+            let mut iter_b2t = self.stack[range].iter();
+            let matcher = |span: &Span| { position.match_string(span.as_str()) };
+            match match_dir {
+                MatchDir::BottomToTop => iter_b2t.all(matcher),
+                MatchDir::TopToBottom => iter_b2t.rev().all(matcher),
+            }
+        };
         if result {
             self.position = position;
             Ok(self)
@@ -937,6 +961,30 @@ impl<'i, R: RuleType> ParserState<'i, R> {
         }
     }
 
+    /// Matches the full state of the stack.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use pest;
+    /// # #[allow(non_camel_case_types)]
+    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+    /// enum Rule {}
+    ///
+    /// let input = "abba";
+    /// let mut state: Box<pest::ParserState<Rule>> = pest::ParserState::new(input);
+    /// let mut result = state
+    ///     .stack_push(|state| state.match_string("a"))
+    ///     .and_then(|state| { state.stack_push(|state| state.match_string("b")) })
+    ///     .and_then(|state| state.stack_match_peek());
+    /// assert!(result.is_ok());
+    /// assert_eq!(result.unwrap().position().pos(), 4);
+    /// ```
+    #[inline]
+    pub fn stack_match_peek(self: Box<Self>) -> ParseResult<Box<Self>> {
+        self.stack_match_peek_slice(0, None, MatchDir::TopToBottom)
+    }
+
     /// Matches the full state of the stack. This method will clear the stack as it evaluates.
     ///
     /// # Examples
@@ -1049,3 +1097,41 @@ impl<'i, R: RuleType> ParserState<'i, R> {
         self
     }
 }
+
+fn constrain_idxs(start: i32, end: Option<i32>, len: usize) -> Option<std::ops::Range<usize>> {
+    let start_norm = normalize_index(start, len)?;
+    let end_norm = end.map_or(Some(len), |e| normalize_index(e, len))?;
+    Some(start_norm..end_norm)
+}
+
+/// Normalizes the index using its sequence’s length.
+/// Returns `None` if the normalized index is OOB.
+fn normalize_index(i: i32, len: usize) -> Option<usize> {
+    if i > len as i32 {
+        None
+    } else if i >= 0 {
+        Some(i as usize)
+    } else {
+        let real_i = len as i32 + i;
+        if real_i >= 0 { Some(real_i as usize) } else { None }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn normalize_index_pos() {
+        assert_eq!(normalize_index(4, 6), Some(4));
+        assert_eq!(normalize_index(5, 5), Some(5));
+        assert_eq!(normalize_index(6, 3), None);
+    }
+
+    #[test]
+    fn normalize_index_neg() {
+        assert_eq!(normalize_index(-4, 6), Some(2));
+        assert_eq!(normalize_index(-5, 5), Some(0));
+        assert_eq!(normalize_index(-6, 3), None);
+    }
+}
diff --git a/pest/src/stack.rs b/pest/src/stack.rs
index 36cde86d..edfd8779 100644
--- a/pest/src/stack.rs
+++ b/pest/src/stack.rs
@@ -7,6 +7,8 @@
 // option. All files in the project carrying such notice may not be copied,
 // modified, or distributed except according to those terms.
 
+use std::ops::{Range, Index};
+
 /// Implementation of a `Stack` which maintains an log of `StackOp`s in order to rewind the stack
 /// to a previous state.
 #[derive(Debug)]
@@ -51,10 +53,10 @@ impl<T: Clone> Stack<T> {
         }
         popped
     }
-
-    /// Returns an iterator to the current state of the cache in fifo order.
-    pub fn iter(&self) -> impl Iterator<Item = &T> {
-        self.cache.iter().rev()
+    
+    /// Returns the size of the stack
+    pub fn len(&self) -> usize {
+        self.cache.len()
     }
 
     /// Takes a snapshot of the current `Stack`.
@@ -93,6 +95,14 @@ impl<T: Clone> Stack<T> {
     }
 }
 
+impl<T: Clone> Index<Range<usize>> for Stack<T> {
+    type Output = [T];
+    
+    fn index(&self, range: Range<usize>) -> &[T] {
+        self.cache.index(range)
+    }
+}
+
 #[derive(Debug)]
 enum StackOp<T> {
     Push(T),
@@ -127,18 +137,7 @@ mod test {
         stack.restore();
         stack.restore();
 
-        assert_eq!(stack.iter().collect::<Vec<&i32>>(), vec![&0]);
-    }
-
-    #[test]
-    fn iter() {
-        let mut stack = Stack::new();
-
-        stack.push(0);
-        stack.push(1);
-        stack.push(2);
-
-        assert_eq!(stack.iter().collect::<Vec<&i32>>(), vec![&2, &1, &0]);
+        assert_eq!(stack[0..stack.len()], [0]);
     }
 
     #[test]
diff --git a/vm/src/lib.rs b/vm/src/lib.rs
index 2741e287..f618220a 100644
--- a/vm/src/lib.rs
+++ b/vm/src/lib.rs
@@ -10,7 +10,7 @@
 extern crate pest;
 extern crate pest_meta;
 
-use pest::{Atomicity, ParseResult, ParserState};
+use pest::{Atomicity, ParseResult, ParserState, MatchDir};
 use pest::error::Error;
 use pest::iterators::Pairs;
 use pest::unicode;
@@ -152,6 +152,7 @@ impl Vm {
                 state.match_range(start..end)
             }
             OptimizedExpr::Ident(ref name) => self.parse_rule(name, state),
+            OptimizedExpr::PeekSlice(start, end) => state.stack_match_peek_slice(start, end, MatchDir::BottomToTop),
             OptimizedExpr::PosPred(ref expr) => {
                 state.lookahead(true, |state| self.parse_expr(expr, state))
             }


Test patch:
diff --git a/derive/tests/grammar.pest b/derive/tests/grammar.pest
index 5155b373..43a7b8cf 100644
--- a/derive/tests/grammar.pest
+++ b/derive/tests/grammar.pest
@@ -38,6 +38,7 @@ soi_at_start = { SOI ~ string }
 repeat_mutate_stack = { (PUSH('a'..'c') ~ ",")* ~ POP ~ POP ~ POP }
 peek_ = { PUSH(range) ~ PUSH(range) ~ PEEK ~ PEEK }
 peek_all = { PUSH(range) ~ PUSH(range) ~ PEEK_ALL }
+peek_slice_23 = { PUSH(range) ~ PUSH(range) ~ PUSH(range) ~ PUSH(range) ~ PUSH(range) ~ PEEK[1..-2] }
 pop_ = { PUSH(range) ~ PUSH(range) ~ POP ~ POP }
 pop_all = { PUSH(range) ~ PUSH(range) ~ POP_ALL }
 pop_fail = { PUSH(range) ~ !POP ~ range ~ POP }
diff --git a/derive/tests/grammar.rs b/derive/tests/grammar.rs
index c218dcfe..d2b8389e 100644
--- a/derive/tests/grammar.rs
+++ b/derive/tests/grammar.rs
@@ -708,6 +708,24 @@ fn peek_all() {
     };
 }
 
+#[test]
+fn peek_slice_23() {
+    parses_to! {
+        parser: GrammarParser,
+        input: "0123412",
+        rule: Rule::peek_slice_23,
+        tokens: [
+            peek_slice_23(0, 7, [
+                range(0, 1),
+                range(1, 2),
+                range(2, 3),
+                range(3, 4),
+                range(4, 5),
+            ])
+        ]
+    };
+}
+
 #[test]
 fn pop() {
     parses_to! {
diff --git a/vm/tests/grammar.rs b/vm/tests/grammar.rs
index 22ec8557..35fb8d6a 100644
--- a/vm/tests/grammar.rs
+++ b/vm/tests/grammar.rs
@@ -715,6 +715,24 @@ fn peek_all() {
     };
 }
 
+#[test]
+fn peek_slice_23() {
+    parses_to! {
+        parser: vm(),
+        input: "0123412",
+        rule: "peek_slice_23",
+        tokens: [
+            peek_slice_23(0, 7, [
+                range(0, 1),
+                range(1, 2),
+                range(2, 3),
+                range(3, 4),
+                range(4, 5),
+            ])
+        ]
+    };
+}
+
 #[test]
 fn pop() {
     parses_to! {