From e2f997a9a18f889f7c92e16eb530e972a2d61d5c Mon Sep 17 00:00:00 2001
From: rschoene <rene.schoene@tu-dresden.de>
Date: Wed, 8 May 2019 19:23:45 +0200
Subject: [PATCH] Begin with expression sub-language.

---
 eraser-base/src/main/jastadd/Expression.jrag  |  10 ++
 .../src/main/jastadd/Expression.relast        |  25 ++++
 eraser-base/src/main/jastadd/Rules.jrag       |   4 +
 eraser-base/src/main/jastadd/Rules.relast     |   2 +
 eraser-base/src/main/jastadd/eraser.flex      |  19 ++-
 eraser-base/src/main/jastadd/eraser.parser    |  61 ++++++++
 .../st/eraser/parser/EraserParserHelper.java  |  27 ++++
 .../inf/st/eraser/util/ParserUtils.java       |  31 +++++
 .../inf/st/eraser/ExpressionParserTest.java   | 131 ++++++++++++++++++
 9 files changed, 308 insertions(+), 2 deletions(-)
 create mode 100644 eraser-base/src/main/jastadd/Expression.jrag
 create mode 100644 eraser-base/src/main/jastadd/Expression.relast
 create mode 100644 eraser-base/src/test/java/de/tudresden/inf/st/eraser/ExpressionParserTest.java

diff --git a/eraser-base/src/main/jastadd/Expression.jrag b/eraser-base/src/main/jastadd/Expression.jrag
new file mode 100644
index 00000000..cadc44e4
--- /dev/null
+++ b/eraser-base/src/main/jastadd/Expression.jrag
@@ -0,0 +1,10 @@
+aspect Expression {
+  syn boolean LogicalExpression.eval() = false;
+  eq NotExpression.eval() = !getOperand().eval();
+  eq ComparingExpression.eval() {
+    // TODO
+    return false;
+  }
+  eq AndExpression.eval() = getLeftOperand().eval() && getRightOperand().eval();
+  eq OrExpression.eval() = getLeftOperand().eval() || getRightOperand().eval();
+}
diff --git a/eraser-base/src/main/jastadd/Expression.relast b/eraser-base/src/main/jastadd/Expression.relast
new file mode 100644
index 00000000..591e112a
--- /dev/null
+++ b/eraser-base/src/main/jastadd/Expression.relast
@@ -0,0 +1,25 @@
+abstract Expression ;
+
+abstract NumberExpression : Expression ;
+ParenthesizedNumberExpression : NumberExpression ::= Operand:NumberExpression ;
+NumberLiteralExpression : NumberExpression ::= <Value:double> ;
+
+// variation point of the expression language:
+Designator : NumberExpression ;
+rel Designator.Item -> Item ;
+
+abstract BinaryNumberExpression : NumberExpression ::= LeftOperand:NumberExpression RightOperand:NumberExpression ;
+AddExpression : BinaryNumberExpression ;
+SubExpression : BinaryNumberExpression ;
+MultExpression : BinaryNumberExpression ;
+DivExpression : BinaryNumberExpression ;
+PowerExpression : BinaryNumberExpression ;
+
+abstract LogicalExpression : Expression ;
+ParenthesizedLogicalExpression : LogicalExpression ::= Operand:LogicalExpression ;
+NotExpression : LogicalExpression ::= Operand:LogicalExpression ;
+ComparingExpression : LogicalExpression ::= LeftOperand:NumberExpression RightOperand:NumberExpression <Comparator:ComparatorType> ;
+
+abstract BinaryLogicalExpression : LogicalExpression ::= LeftOperand:LogicalExpression RightOperand:LogicalExpression ;
+AndExpression : BinaryLogicalExpression ;
+OrExpression : BinaryLogicalExpression ;
diff --git a/eraser-base/src/main/jastadd/Rules.jrag b/eraser-base/src/main/jastadd/Rules.jrag
index 69ae87d3..26c7e996 100644
--- a/eraser-base/src/main/jastadd/Rules.jrag
+++ b/eraser-base/src/main/jastadd/Rules.jrag
@@ -78,9 +78,13 @@ aspect Rules {
   // --- Condition.holdsFor ---
   syn boolean Condition.holdsFor(Item item);
   eq ItemStateCheckCondition.holdsFor(Item item) = getItemStateCheck().holdsFor(item);
+  eq ExpressionCondition.holdsFor(Item item) = getLogicalExpression().eval();
 
   // --- Action.applyFor ---
   public abstract void Action.applyFor(Item item);
+  public void NoopAction.applyFor(Item item) {
+    // empty by design
+  }
   public void LambdaAction.applyFor(Item item) {
     getLambda().accept(item);
   }
diff --git a/eraser-base/src/main/jastadd/Rules.relast b/eraser-base/src/main/jastadd/Rules.relast
index 69854a70..88f0c3d6 100644
--- a/eraser-base/src/main/jastadd/Rules.relast
+++ b/eraser-base/src/main/jastadd/Rules.relast
@@ -2,7 +2,9 @@
 Rule ::= Condition* Action* ;
 abstract Condition ;
 ItemStateCheckCondition : Condition ::= ItemStateCheck ;
+ExpressionCondition : Condition ::= LogicalExpression ;
 abstract Action ;
+NoopAction : Action ;
 LambdaAction : Action ::= <Lambda:Action2EditConsumer> ;
 
 TriggerRuleAction : Action ;
diff --git a/eraser-base/src/main/jastadd/eraser.flex b/eraser-base/src/main/jastadd/eraser.flex
index 58d16b9c..ffb7bdeb 100644
--- a/eraser-base/src/main/jastadd/eraser.flex
+++ b/eraser-base/src/main/jastadd/eraser.flex
@@ -34,7 +34,7 @@ WhiteSpace = [ ] | \t | \f | \n | \r | \r\n
 Text = \" ([^\"]*) \"
 
 Integer = [:digit:]+ // | "+" [:digit:]+ | "-" [:digit:]+
-//Real    = [:digit:]+ "." [:digit:]* | "." [:digit:]+
+Real    = [:digit:]+ "." [:digit:]* | "." [:digit:]+
 
 Comment = "//" [^\n\r]+
 
@@ -56,6 +56,7 @@ Comment = "//" [^\n\r]+
 "Mqtt"         { return sym(Terminals.MQTT); }
 "Influx"       { return sym(Terminals.INFLUX); }
 "ML"           { return sym(Terminals.ML); }
+"Rule"         { return sym(Terminals.RULE); }
 // special items (group already has a token definition)
 "Activity"     { return sym(Terminals.ACTIVITY); }
 "Color"        { return sym(Terminals.COLOR); }
@@ -101,6 +102,20 @@ Comment = "//" [^\n\r]+
 // special characters
 "="            { return sym(Terminals.EQUALS); }
 //"\""           { return sym(Terminals.QUOTE); }
+"<"            { return sym(Terminals.LE); }
+"<="           { return sym(Terminals.LT); }
+"=="           { return sym(Terminals.EQ); }
+"!="           { return sym(Terminals.NE); }
+">"            { return sym(Terminals.GT); }
+">="           { return sym(Terminals.GE); }
+"+"            { return sym(Terminals.PLUS); }
+"*"            { return sym(Terminals.MULT); }
+"-"            { return sym(Terminals.MINUS); }
+"/"            { return sym(Terminals.DIV); }
+"^"            { return sym(Terminals.POW); }
+"!"            { return sym(Terminals.EXCLAMATION); }
+"|"            { return sym(Terminals.OR); }
+"&"            { return sym(Terminals.AND); }
 ":"            { return sym(Terminals.COLON); }
 ","            { return sym(Terminals.COMMA); }
 ";"            { return sym(Terminals.SEMICOLON); }
@@ -112,8 +127,8 @@ Comment = "//" [^\n\r]+
 "}"            { return sym(Terminals.RB_CURLY); }
 //{Identifier}   { return sym(Terminals.NAME); }
 {Text}         { return symText(Terminals.TEXT); }
-//{Real}         { return sym(Terminals.REAL); }
 {Integer}      { return sym(Terminals.INTEGER); }
+{Real}         { return sym(Terminals.REAL); }
 <<EOF>>        { return sym(Terminals.EOF); }
 /* error fallback */
 [^]            { throw new Error("Illegal character '"+ yytext() +"' at line " + (yyline+1) + " column " + (yycolumn+1)); }
diff --git a/eraser-base/src/main/jastadd/eraser.parser b/eraser-base/src/main/jastadd/eraser.parser
index acd54d7c..0639a0fa 100644
--- a/eraser-base/src/main/jastadd/eraser.parser
+++ b/eraser-base/src/main/jastadd/eraser.parser
@@ -1,6 +1,7 @@
 %header {:
 package de.tudresden.inf.st.eraser.jastadd.parser;
 import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.jastadd.model.Action;
 import de.tudresden.inf.st.eraser.parser.EraserParserHelper;
 import java.util.Map;
 import java.util.HashMap;
@@ -23,6 +24,8 @@ import java.util.HashMap;
 :} ;
 
 %goal goal;
+%goal number_expression;
+%goal logical_expression;
 
 Root goal =
      thing.t goal.r                     {: insertZero(r.getOpenHAB2Model().getThingList(), t); return r; :}
@@ -35,6 +38,7 @@ Root goal =
   |  mqtt_root.mr goal.r                {: r.setMqttRoot(mr); return r; :}
   |  influx_root.ir goal.r              {: r.setInfluxRoot(ir); return r; :}
   |  machine_learning_root.ml goal.r    {: r.setMachineLearningRoot(ml); return r; :}
+  |  rule.rule goal.r                   {: r.addRule(rule); return r; :}
   |  thing.t                            {: return eph.createRoot(t); :}
   |  item.i                             {: return eph.createRoot(); :}
   |  group.g                            {: return eph.createRoot(g); :}
@@ -45,6 +49,48 @@ Root goal =
   |  mqtt_root.mr                       {: return eph.createRoot(mr); :}
   |  influx_root.ir                     {: return eph.createRoot(ir); :}
   |  machine_learning_root.ml           {: return eph.createRoot(ml); :}
+  |  rule.rule                          {: return eph.createRoot(rule); :}
+  ;
+
+%left RB_ROUND;
+%left MULT, DIV;
+%left PLUS, MINUS;
+%left POW;
+%left LT, LE, EQ, GE, GT;
+%left OR;
+%left AND;
+
+NumberExpression number_expression =
+    LB_ROUND number_expression.a MULT  number_expression.b RB_ROUND     {: return new MultExpression(a, b); :}
+  | LB_ROUND number_expression.a DIV   number_expression.b RB_ROUND     {: return new DivExpression(a, b); :}
+  | LB_ROUND number_expression.a PLUS  number_expression.b RB_ROUND     {: return new AddExpression(a, b); :}
+  | LB_ROUND number_expression.a MINUS number_expression.b RB_ROUND     {: return new SubExpression(a, b); :}
+  | LB_ROUND number_expression.a POW number_expression.b RB_ROUND       {: return new PowerExpression(a, b); :}
+  | literal_expression.l                                                {: return l; :}
+  | designator.d                                                        {: return d; :}
+  | LB_ROUND number_expression.e RB_ROUND                               {: return new ParenthesizedNumberExpression(e); :}
+  ;
+
+LogicalExpression logical_expression =
+    LB_ROUND logical_expression.a AND   logical_expression.b RB_ROUND     {: return new AndExpression(a, b); :}
+  | LB_ROUND logical_expression.a OR    logical_expression.b RB_ROUND     {: return new OrExpression(a, b); :}
+  | LB_ROUND EXCLAMATION logical_expression.e RB_ROUND                    {: return new NotExpression(e); :}
+  | LB_ROUND logical_expression.e RB_ROUND                                {: return new ParenthesizedLogicalExpression(e); :}
+  | LB_ROUND number_expression.a LT    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.LessThan); :}
+  | LB_ROUND number_expression.a LE    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.LessOrEqualThan); :}
+  | LB_ROUND number_expression.a EQ    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.Equals); :}
+  | LB_ROUND number_expression.a NE    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.NotEquals); :}
+  | LB_ROUND number_expression.a GE    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.GreaterOrEqualThan); :}
+  | LB_ROUND number_expression.a GT    number_expression.b RB_ROUND       {: return new ComparingExpression(a, b, ComparatorType.GreaterThan); :}
+  ;
+
+NumberLiteralExpression literal_expression =
+    INTEGER.n             {: return new NumberLiteralExpression(Integer.parseInt(n)); :}
+  | REAL.n                {: return new NumberLiteralExpression(Double.parseDouble(n)); :}
+  ;
+
+Designator designator =
+    TEXT.n                    {: return eph.createDesignator(n); :}
   ;
 
 Thing thing =
@@ -208,6 +254,21 @@ MachineLearningRoot machine_learning_root_body =
   |                                                                   {: return MachineLearningRoot.createDefault(); :}
   ;
 
+// Rule: condition=condition action=a1 action=a2;
+// (only allow one condition and action for now)
+Rule rule =
+    RULE COLON CONDITION EQUALS condition.c action.a SEMICOLON          {: return eph.createRule(c, a); :}
+  ;
+
+Condition condition =
+    logical_expression.be                            {: return new ExpressionCondition(be); :}
+  ;
+
+// TODO implement action cases
+Action action = {: return new NoopAction(); :}
+  ;
+
+// Util: StringList, StringKeyMap, IntegerKeyMap
 StringList string_list =
      LB_SQUARE string_list_body.slb RB_SQUARE         {: return slb; :}
   |  LB_SQUARE RB_SQUARE                              {: return new StringList(); :}
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java
index 400a09af..bc74bdd9 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/parser/EraserParserHelper.java
@@ -7,6 +7,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.util.*;
+import java.util.Comparator;
 import java.util.function.BiConsumer;
 
 /**
@@ -29,6 +30,7 @@ public class EraserParserHelper {
   private Map<Channel, String> missingChannelTypeMap = new HashMap<>();
   private Map<Item, String> missingTopicMap = new HashMap<>();
   private Map<Item, String> missingItemCategoryMap = new HashMap<>();
+  private Map<Designator, String> missingItemForDesignator = new HashMap<>();
 
   private Map<Thing, Iterable<String>> missingChannelListMap = new HashMap<>();
   private Map<Channel, Iterable<String>> missingItemLinkListMap = new HashMap<>();
@@ -64,11 +66,16 @@ public class EraserParserHelper {
    * @throws java.util.NoSuchElementException if a reference can not be resolved
    */
   public void resolveReferences() {
+    if (this.root == null) {
+      // when parsing expressions
+      this.root = createRoot();
+    }
     if (checkUnusedElements) {
       fillUnused();
     }
     resolve(thingTypeMap, missingThingTypeMap, Thing::setType);
     resolve(channelTypeMap, missingChannelTypeMap, Channel::setType);
+    resolve(itemMap, missingItemForDesignator, Designator::setItem);
     missingTopicMap.forEach((topic, parts) -> ParserUtils.createMqttTopic(topic, parts, this.root));
     this.root.getMqttRoot().ensureCorrectPrefixes();
 
@@ -448,4 +455,24 @@ public class EraserParserHelper {
     return result;
   }
 
+  public Root createRoot(Rule rule) {
+    Root result = createRoot();
+    result.addRule(rule);
+    return result;
+  }
+
+  //+++ newStuff (to be categorized) +++
+  public Designator createDesignator(String itemName) {
+    Designator result = new Designator();
+    missingItemForDesignator.put(result, itemName);
+    return result;
+  }
+
+  public Rule createRule(Condition c, Action a) {
+    Rule result = new Rule();
+    result.addCondition(c);
+    result.addAction(a);
+    return result;
+  }
+
 }
diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java
index 5eca1e63..657a2808 100644
--- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java
+++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java
@@ -192,4 +192,35 @@ public class ParserUtils {
     }));
   }
 
+  public static NumberExpression parseNumberExpression(String expression_string) throws IOException, Parser.Exception {
+    return (NumberExpression) parseExpression(expression_string, EraserParser.AltGoals.number_expression);
+  }
+
+  public static LogicalExpression parseLogicalExpression(String expression_string) throws IOException, Parser.Exception {
+    return (LogicalExpression) parseExpression(expression_string, EraserParser.AltGoals.logical_expression);
+  }
+
+  private static Expression parseExpression(String expression_string, short alt_goal) throws IOException, Parser.Exception {
+    StringReader reader = new StringReader(expression_string);
+    if (verboseLoading) {
+      EraserScanner scanner = new EraserScanner(reader);
+      try {
+        Symbol token;
+        while ((token = scanner.nextToken()).getId() != EraserParser.Terminals.EOF) {
+          logger.debug("start: {}, end: {}, id: {}, value: {}",
+              token.getStart(), token.getEnd(), EraserParser.Terminals.NAMES[token.getId()], token.value);
+        }
+      } catch (Scanner.Exception e) {
+        e.printStackTrace();
+      }
+    }
+    reader = new StringReader(expression_string);
+    EraserScanner scanner = new EraserScanner(reader);
+    EraserParser parser = new EraserParser();
+    Expression result = (Expression) parser.parse(scanner, alt_goal);
+    parser.resolveReferences();
+    reader.close();
+    return result;
+  }
+
 }
diff --git a/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ExpressionParserTest.java b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ExpressionParserTest.java
new file mode 100644
index 00000000..a7f95965
--- /dev/null
+++ b/eraser-base/src/test/java/de/tudresden/inf/st/eraser/ExpressionParserTest.java
@@ -0,0 +1,131 @@
+package de.tudresden.inf.st.eraser;
+
+import beaver.Parser;
+import de.tudresden.inf.st.eraser.jastadd.model.*;
+import de.tudresden.inf.st.eraser.util.ParserUtils;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Test correct parsing of NumberExpression and LogicalExpression.
+ *
+ * @author rschoene - Initial contribution
+ */
+public class ExpressionParserTest {
+
+  @Test
+  public void plusExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(3 + 4)");
+    assertThat(sut, instanceOf(AddExpression.class));
+    AddExpression addExpression = (AddExpression) sut;
+    assertThat(addExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) addExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(3.0));
+    assertThat(addExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) addExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(4.0));
+  }
+
+  @Test
+  public void minusExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(12.5 - 4.1)");
+    assertThat(sut, instanceOf(SubExpression.class));
+    SubExpression subExpression = (SubExpression) sut;
+    assertThat(subExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) subExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(12.5));
+    assertThat(subExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) subExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(4.1));
+  }
+
+  @Test
+  public void multExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(0 * 0)");
+    assertThat(sut, instanceOf(MultExpression.class));
+    MultExpression multExpression = (MultExpression) sut;
+    assertThat(multExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) multExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(0.0));
+    assertThat(multExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) multExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(0.0));
+  }
+
+  @Test
+  public void divExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(1.1 / 0.0)");
+    assertThat(sut, instanceOf(DivExpression.class));
+    DivExpression divExpression = (DivExpression) sut;
+    assertThat(divExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) divExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(1.1));
+    assertThat(divExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) divExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(0.0));
+  }
+
+  @Test
+  public void powerExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(3 ^ 0.5)");
+    assertThat(sut, instanceOf(PowerExpression.class));
+    PowerExpression powExpression = (PowerExpression) sut;
+    assertThat(powExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) powExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(3.0));
+    assertThat(powExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) powExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(0.5));
+  }
+
+  @Test
+  public void parenthesizedExpression() throws IOException, Parser.Exception {
+    NumberExpression sut = ParserUtils.parseNumberExpression("(3)");
+    assertThat(sut, instanceOf(ParenthesizedNumberExpression.class));
+    ParenthesizedNumberExpression parenExpression = (ParenthesizedNumberExpression) sut;
+    assertThat(parenExpression.getOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) parenExpression.getOperand();
+    assertThat(left.getValue(), equalTo(3.0));
+  }
+
+  @Test
+  public void complexExpression() throws IOException, Parser.Exception {
+    ParserUtils.setVerboseLoading(true);
+    NumberExpression sut = ParserUtils.parseNumberExpression("((3 + 4) * (1 / (12 - 8)))");
+    assertThat(sut, instanceOf(MultExpression.class));
+    MultExpression multExpression = (MultExpression) sut;
+
+    // 3+4
+    assertThat(multExpression.getLeftOperand(), instanceOf(AddExpression.class));
+    AddExpression addExpression = (AddExpression) multExpression.getLeftOperand();
+    assertThat(addExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression left = (NumberLiteralExpression) addExpression.getLeftOperand();
+    assertThat(left.getValue(), equalTo(3.0));
+    assertThat(addExpression.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression right = (NumberLiteralExpression) addExpression.getRightOperand();
+    assertThat(right.getValue(), equalTo(4.0));
+
+    // 1/(12-8)
+    assertThat(multExpression.getRightOperand(), instanceOf(DivExpression.class));
+    DivExpression divExpression = (DivExpression) multExpression.getRightOperand();
+
+    assertThat(divExpression.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression leftOfDiv = (NumberLiteralExpression) divExpression.getLeftOperand();
+    assertThat(leftOfDiv.getValue(), equalTo(1.0));
+
+    // 12-8
+    assertThat(divExpression.getRightOperand(), instanceOf(SubExpression.class));
+    SubExpression rightofDiv = (SubExpression) divExpression.getRightOperand();
+    assertThat(rightofDiv.getLeftOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression leftOfSSub = (NumberLiteralExpression) rightofDiv.getLeftOperand();
+    assertThat(leftOfSSub.getValue(), equalTo(12.0));
+    assertThat(rightofDiv.getRightOperand(), instanceOf(NumberLiteralExpression.class));
+    NumberLiteralExpression rightOfSub = (NumberLiteralExpression) rightofDiv.getRightOperand();
+    assertThat(rightOfSub.getValue(), equalTo(8.0));
+  }
+}
-- 
GitLab