diff --git a/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala b/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala index 7b9ab5ef..4f6fdaf1 100644 --- a/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala +++ b/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala @@ -20,6 +20,15 @@ import scala.language.implicitConversions // TODO: better error handling (labelling like parsec's ) +/** An enumeration of operator associativity values: `Left`, `Right`, and + * `Non`. + */ +object Associativity extends Enumeration { + type Associativity = Value + + val Left, Right, Non = Value +} + /** `Parsers` is a component that ''provides'' generic parser combinators. * * There are two abstract members that must be defined in order to @@ -1037,4 +1046,95 @@ trait Parsers { override def <~ [U](p: => Parser[U]): Parser[T] = OnceParser{ (for(a <- this; _ <- commit(p)) yield a).named("<~") } } + + import Associativity._ + + /** A parser that respects operator the precedence and associativity + * conventions specified in its constructor. + * + * @param primary a parser that matches atomic expressions (the atomicity is + * from the perspective of binary operators). May include + * unary operators or parentheses. + * @param binop a parser that matches binary operators. + * @param prec_table a list of tuples, each of which encodes a level of + * precedence. Precedence is encoded highest to lowest. + * Each precedence level contains an Associativity value + * and a list of operators. + * @param makeBinop a function that combines two operands and an operator + * into a new expression. The result must have the same type + * as the operands because intermediate results become + * operands to other operators. + */ + class PrecedenceParser[Exp,Op,E <: Exp](primary: Parser[E], + binop: Parser[Op], + prec_table: List[(Associativity, List[Op])], + makeBinop: (Exp, Op, Exp) => Exp) extends Parser[Exp] { + private def decodePrecedence: (Map[Op, Int], Map[Op, Associativity]) = { + var precedence = Map.empty[Op, Int] + var associativity = Map.empty[Op, Associativity] + var level = prec_table.length + for ((assoc, ops) <- prec_table) { + precedence = precedence ++ (for (op <- ops) yield (op, level)) + associativity = associativity ++ (for (op <- ops) yield (op, assoc)) + level -= 1 + } + (precedence, associativity) + } + val (precedence, associativity) = decodePrecedence + private class ExpandLeftParser(lhs: Exp, minLevel: Int) extends Parser[Exp] { + def apply(input: Input): ParseResult[Exp] = { + (binop ~ primary)(input) match { + case Success(op ~ rhs, next) if precedence(op) >= minLevel => { + new ExpandRightParser(rhs, precedence(op), minLevel)(next) match { + case Success(r, nextInput) => new ExpandLeftParser(makeBinop(lhs, op, r), minLevel)(nextInput); + case ns => ns // dead code + } + } + case _ => { + Success(lhs, input); + } + } + } + } + + private class ExpandRightParser(rhs: Exp, currentLevel: Int, minLevel: Int) extends Parser[Exp] { + private def nextLevel(nextBinop: Op): Option[Int] = { + if (precedence(nextBinop) > currentLevel) { + Some(minLevel + 1) + } else if (precedence(nextBinop) == currentLevel && associativity(nextBinop) == Associativity.Right) { + Some(minLevel) + } else { + None + } + } + def apply(input: Input): ParseResult[Exp] = { + def done: ParseResult[Exp] = Success(rhs, input) + binop(input) match { + case Success(nextBinop,_) => { + nextLevel(nextBinop) match { + case Some(level) => { + new ExpandLeftParser(rhs, level)(input) match { + case Success(r, next) => new ExpandRightParser(r, currentLevel, minLevel)(next) + case ns => ns // dead code + } + } + case None => done + } + } + case _ => done + } + } + } + + /** Parse an expression. + */ + def apply(input: Input): ParseResult[Exp] = { + primary(input) match { + case Success(lhs, next) => { + new ExpandLeftParser(lhs,0)(next) + } + case noSuccess => noSuccess + } + } + } } diff --git a/shared/src/test/scala/scala/util/parsing/combinator/PrecedenceParserTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/PrecedenceParserTest.scala new file mode 100644 index 00000000..ab004f27 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/PrecedenceParserTest.scala @@ -0,0 +1,79 @@ +package scala.util.parsing.combinator + +// import scala.language.implicitConversions + +import java.io.StringReader +import org.junit.Test +import org.junit.Assert.{ assertEquals, assertTrue, fail } +import scala.util.parsing.input.StreamReader + +class PrecedenceParsersTest { + + abstract class Op + object Plus extends Op { + override def toString = "+" + } + object Minus extends Op { + override def toString = "-" + } + object Mult extends Op { + override def toString = "*" + } + object Divide extends Op { + override def toString = "/" + } + object Equals extends Op { + override def toString = "=" + } + + abstract class Node + case class Leaf(v: Int) extends Node { + override def toString = v.toString + } + case class Binop(lhs: Node, op: Op, rhs: Node) extends Node { + override def toString = s"($lhs $op $rhs)" + } + + object ArithmeticParser extends RegexParsers { + val prec = List( + (Associativity.Left, List(Mult, Divide)), + (Associativity.Left, List(Plus, Minus)), + (Associativity.Right, List(Equals))) + def integer: Parser[Leaf] = "[0-9]+".r ^^ { (s: String) => Leaf(s.toInt) } + def binop: Parser[Op] = "+" ^^^ Plus | "-" ^^^ Minus | "*" ^^^ Mult | "/" ^^^ Divide | "=" ^^^ Equals + def expression = new PrecedenceParser(integer, binop, prec, Binop.apply) + } + + def testExp(expected: Node, input: String): Unit = { + ArithmeticParser.expression(StreamReader(new StringReader(input))) match { + case ArithmeticParser.Success(r, next) => { + assertEquals(expected, r); + assertTrue(next.atEnd); + } + case e => { + fail(s"Error parsing $input: $e"); + } + } + } + + @Test + def basicExpTests: Unit = { + testExp(Leaf(4), "4") + testExp(Binop(Leaf(1), Plus, Leaf(2)), "1 + 2") + testExp(Binop(Leaf(2), Mult, Leaf(1)), "2 * 1") + } + + @Test + def associativityTests: Unit = { + testExp(Binop(Binop(Leaf(1), Minus, Leaf(2)), Plus, Leaf(3)), "1 - 2 + 3") + testExp(Binop(Leaf(1), Equals, Binop(Leaf(2), Equals, Leaf(3))), "1 = 2 = 3") + } + + @Test + def precedenceTests: Unit = { + testExp(Binop(Binop(Leaf(0), Mult, Leaf(5)), Minus, Leaf(2)), "0 * 5 - 2") + testExp(Binop(Leaf(3), Plus, Binop(Leaf(9), Divide, Leaf(11))), "3 + 9 / 11") + testExp(Binop(Binop(Leaf(6), Plus, Leaf(8)), Equals, Leaf(1)), "6 + 8 = 1") + testExp(Binop(Leaf(4), Equals, Binop(Leaf(5), Minus, Leaf(3))), "4 = 5 - 3") + } +}