Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unary prefix/suffix operator support for PrecClimber #344

Closed
wants to merge 1 commit into from

Conversation

segeljakt
Copy link
Contributor

@segeljakt segeljakt commented Nov 23, 2018

Adds a new PrattParser, which works similar to PrecClimber but extends it with unary (prefix and suffix) operator handling.

To try it out, checkout this PR and run cargo run --example=calc in the terminal.

Given a grammar:

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) }

You can now define a PrattParser:

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));

And use it like:

fn parse_to_i32(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_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)
}

Some things to consider:

  • Does not support ternary operators.
  • Precedence climbing like before uses recursion, though I'm not sure how deep you need to go for this to become a problem.
  • The current naming (prefix/suffix/infix) could be misinterpreted as binary operators with prefix/suffix/infix notation. Does it work, or should I name things more explicitly, e.g., unary_prefix/unary_suffix/binary_infix?

@segeljakt segeljakt changed the title Prec climber arity Unary operator support for PrecClimber Nov 23, 2018
@segeljakt segeljakt changed the title Unary operator support for PrecClimber Unary prefix/suffix operator support for PrecClimber Nov 23, 2018
Copy link
Contributor

@dragostis dragostis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for your contribution! Really interesting work. I've taken a first look over the PR and there are a few issues that need to be resolved in order to get closer to merging this:

  • we need to find a way to support this in 2.1+; the way I see it, 3.0 is too far right now away in order lock into a particular implementation, so we need to find a way to deprecate the old types without breaking compatibility
  • prefix and postfix/suffix operators should potentially have their own precedence; this would require changing the algorithm
  • we should probably add another type in order not to restrain verification only in debug builds

}
}

/// Defines `rule` as a suffix unary operator.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more in favour of postfix rather than suffix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// Below is a [`PrecClimber`] that is able to climb 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. In the current version, prefix operators precede suffix, and suffix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue here is that users might find confusing how, even though infix, prefix and postfix operators are mixed together, order matters only when Ops are infix. In other words, there is a redundant ordering complication here that has potential to be confusing.

In my opinion, the best course of action is to either logically separate prefix/postfix ops, such that they cannot be mixed in with infix operators, or extend the algorithm to deal with prefix/postfix operator precedence as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The algorithm should now be able to deal with prefix/postfix operator precedence

/// _ => unreachable!(),
/// })
/// .map_suffix(|lhs, op| match op.as_rule() {
/// Rule::fac => (1..=lhs).product(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of a papercut, but I believe this syntax will probably raise the minimum required Rust version. Might be a good idea to leave these kind of refactorings for when we tackle the 2018 edition refactor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to (1..lhs+1)

///
/// [`map_primary`]: struct.PrecClimber.html#method.map_primary
/// [`PrecClimber`]: struct.PrecClimber.html
pub struct PrecClimberMap<'climber, 'pest, R, F, T>
where
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look like the style specified in rustfmt.toml, but I might be mistaken. Is this formatted with rustfmt? Trailing commas style also seems to be different.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should now be properly formatted

pairs: P
) -> T {

#[cfg(debug_assertions)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be worthwhile to introduce a third type that this one would get compiled to. This would both offer a nice API, i.e. return a None if missing map_*s, and relive the user from possible headaches in release builds.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed so that the code panics either when:

  • The stream of tokens does not match the expected pattern.
  • No map function is specified for a certain kind of operator.

}

/// Maps primary expressions with a closure `primary`.
pub fn map_primary<'climber, 'pest, X, T>(&'climber self, primary: X)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'i in pest stands for input. 'pest lifetime is too ambiguous.

Suggested change
pub fn map_primary<'climber, 'pest, X, T>(&'climber self, primary: X)
pub fn map_primary<'climber, 'i, X, T>(&'climber self, primary: X)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@segeljakt segeljakt force-pushed the prec-climber-arity branch 5 times, most recently from 725007f to fce43c4 Compare December 2, 2018 20:46
@@ -187,14 +190,15 @@ fn expression() {
}

#[test]
fn prec_climb() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see tests for PrecClimber removed and replaced, but PrecClimber is still in the codebase. I don't think these should be removed until PrecClimber itself is.

@agausmann
Copy link

This would be a great feature to have! Personally I think the right way to incorporate this is to keep the public APIs involving PrecClimber at least until next major release, but supplement them with newer APIs that use PrattParser instead. There's nothing wrong with keeping two different algorithms around for a little while.

@segeljakt
Copy link
Contributor Author

I recently added this to the pest-ast crate, you can use it like this with a grammar like this.

@lberezy
Copy link

lberezy commented Feb 25, 2020

I understand that this is a little bit old now and would probably requite a bit of fixing up, but is there any movement on this?

I think it would be quite handy to enable prefix/suffix operators like this.

@dragostis
Copy link
Contributor

@lberezy, there hasn't been that much work here ever since, but I'm happy to merge this if it gets push forward.

@segeljakt
Copy link
Contributor Author

segeljakt commented Apr 6, 2020

Hi again, I refactored this into its own crate https://crates.io/crates/pratt. Feel free to copy it, add it as a dependency, or just point to it from to pest. You may close this PR if you want.

tomtau pushed a commit to tomtau/pest that referenced this pull request Sep 20, 2022
Closes pest-parser#461

based on pest-parser#344

Co-authored-by: Klas Segeljakt <klasseg@kth.se>
@tomtau
Copy link
Contributor

tomtau commented Sep 20, 2022

@segeljakt @lberezy @dragostis I opened a new PR based on this one: #710 -- I had to make a few small changes in order to support no_std. I kept the original PrecClimber in order not to break the existing public API, but marked it with the #[deprecated] annotation.

@tomtau tomtau closed this Sep 20, 2022
tomtau added a commit that referenced this pull request Oct 1, 2022
Closes #461

based on #344

Co-authored-by: Tomas Tauber <me@tomtau.be>
Co-authored-by: Klas Segeljakt <klasseg@kth.se>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants