├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── antlr4 │ └── me │ │ └── ivanyu │ │ └── RuleSetGrammar.g4 └── java │ └── me │ └── ivanyu │ ├── CompilerApplication.java │ ├── compiler │ ├── Compiler.java │ ├── ExceptionThrowingErrorHandler.java │ └── TreeBuilder.java │ └── pojos │ ├── AndExpression.java │ ├── ArithmeticExpression.java │ ├── ComparisonExpression.java │ ├── ComparisonOperand.java │ ├── Conclusion.java │ ├── LogicalConstant.java │ ├── LogicalExpression.java │ ├── LogicalVariable.java │ ├── Negation.java │ ├── NumericConstant.java │ ├── NumericEntity.java │ ├── NumericVariable.java │ ├── OrExpression.java │ ├── RealArithmeticExpression.java │ ├── Rule.java │ ├── RuleSet.java │ ├── RuleSetPojo.java │ └── serializers │ └── ConclusionSerializer.java └── test └── java └── me └── ivanyu ├── CompilerTest.java └── GrammarTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | /target 14 | /rule-compiler.iml 15 | /.idea 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ivan Yurchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logical-rules-parser-antlr 2 | ========================== 3 | 4 | A simple example of a parser built with ANTLR. 5 | 6 | There is [the blog post](http://ivanyu.me/blog/2014/09/13/creating-a-simple-parser-with-antlr/) describes the parser. 7 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | me.ivanyu 6 | rule-compiler 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | rule-compiler 11 | http://maven.apache.org 12 | 13 | 14 | UTF-8 15 | UTF-8 16 | 17 | 18 | 19 | 20 | junit 21 | junit 22 | 4.11 23 | test 24 | 25 | 26 | 27 | com.fasterxml.jackson.core 28 | jackson-annotations 29 | 2.3.0 30 | 31 | 32 | com.fasterxml.jackson.core 33 | jackson-databind 34 | 2.3.3 35 | 36 | 37 | com.fasterxml.jackson.core 38 | jackson-core 39 | 2.3.1 40 | 41 | 42 | 43 | org.antlr 44 | antlr4-runtime 45 | 4.3 46 | 47 | 48 | 49 | 50 | 51 | 52 | org.antlr 53 | antlr4-maven-plugin 54 | 4.3 55 | 56 | 57 | 58 | antlr4 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-compiler-plugin 67 | 3.1 68 | 69 | 1.7 70 | 1.7 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/antlr4/me/ivanyu/RuleSetGrammar.g4: -------------------------------------------------------------------------------- 1 | grammar RuleSetGrammar; 2 | 3 | /* Lexical rules */ 4 | 5 | IF : 'if' ; 6 | THEN : 'then'; 7 | 8 | AND : 'and' ; 9 | OR : 'or' ; 10 | 11 | TRUE : 'true' ; 12 | FALSE : 'false' ; 13 | 14 | MULT : '*' ; 15 | DIV : '/' ; 16 | PLUS : '+' ; 17 | MINUS : '-' ; 18 | 19 | GT : '>' ; 20 | GE : '>=' ; 21 | LT : '<' ; 22 | LE : '<=' ; 23 | EQ : '=' ; 24 | 25 | LPAREN : '(' ; 26 | RPAREN : ')' ; 27 | 28 | // DECIMAL, IDENTIFIER, COMMENTS, WS are set using regular expressions 29 | 30 | DECIMAL : '-'?[0-9]+('.'[0-9]+)? ; 31 | 32 | IDENTIFIER : [a-zA-Z_][a-zA-Z_0-9]* ; 33 | 34 | SEMI : ';' ; 35 | 36 | // COMMENT and WS are stripped from the output token stream by sending 37 | // to a different channel 'skip' 38 | 39 | COMMENT : '//' .+? ('\n'|EOF) -> skip ; 40 | 41 | WS : [ \r\t\u000C\n]+ -> skip ; 42 | 43 | 44 | /* Parser rules */ 45 | 46 | rule_set : single_rule* EOF ; 47 | 48 | single_rule : IF condition THEN conclusion SEMI ; 49 | 50 | condition : logical_expr ; 51 | conclusion : IDENTIFIER ; 52 | 53 | logical_expr 54 | : logical_expr AND logical_expr # LogicalExpressionAnd 55 | | logical_expr OR logical_expr # LogicalExpressionOr 56 | | comparison_expr # ComparisonExpression 57 | | LPAREN logical_expr RPAREN # LogicalExpressionInParen 58 | | logical_entity # LogicalEntity 59 | ; 60 | 61 | comparison_expr : comparison_operand comp_operator comparison_operand 62 | # ComparisonExpressionWithOperator 63 | | LPAREN comparison_expr RPAREN # ComparisonExpressionParens 64 | ; 65 | 66 | comparison_operand : arithmetic_expr 67 | ; 68 | 69 | comp_operator : GT 70 | | GE 71 | | LT 72 | | LE 73 | | EQ 74 | ; 75 | 76 | arithmetic_expr 77 | : arithmetic_expr MULT arithmetic_expr # ArithmeticExpressionMult 78 | | arithmetic_expr DIV arithmetic_expr # ArithmeticExpressionDiv 79 | | arithmetic_expr PLUS arithmetic_expr # ArithmeticExpressionPlus 80 | | arithmetic_expr MINUS arithmetic_expr # ArithmeticExpressionMinus 81 | | MINUS arithmetic_expr # ArithmeticExpressionNegation 82 | | LPAREN arithmetic_expr RPAREN # ArithmeticExpressionParens 83 | | numeric_entity # ArithmeticExpressionNumericEntity 84 | ; 85 | 86 | logical_entity : (TRUE | FALSE) # LogicalConst 87 | | IDENTIFIER # LogicalVariable 88 | ; 89 | 90 | numeric_entity : DECIMAL # NumericConst 91 | | IDENTIFIER # NumericVariable 92 | ; 93 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/CompilerApplication.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.SerializationFeature; 6 | import me.ivanyu.compiler.Compiler; 7 | import me.ivanyu.pojos.RuleSet; 8 | 9 | public class CompilerApplication { 10 | public static void main(String[] args) { 11 | Compiler compiler = new Compiler(); 12 | RuleSet ruleSet = compiler.compile("if -(A + 2) > 0.5 then be_careful;"); 13 | 14 | // JSON serialization 15 | ObjectMapper mapper = new ObjectMapper(); 16 | mapper.enable(SerializationFeature.INDENT_OUTPUT); 17 | 18 | String jsonString = null; 19 | try { 20 | jsonString = mapper.writeValueAsString(ruleSet); 21 | } catch (JsonProcessingException e) { 22 | e.printStackTrace(); 23 | } 24 | System.out.println(jsonString); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/compiler/Compiler.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.compiler; 2 | 3 | import me.ivanyu.RuleSetGrammarLexer; 4 | import me.ivanyu.RuleSetGrammarParser; 5 | import me.ivanyu.pojos.RuleSet; 6 | import org.antlr.v4.runtime.ANTLRInputStream; 7 | import org.antlr.v4.runtime.CommonTokenStream; 8 | import org.antlr.v4.runtime.TokenStream; 9 | 10 | public class Compiler { 11 | public RuleSet compile(String inputString) { 12 | ANTLRInputStream input = new ANTLRInputStream(inputString); 13 | RuleSetGrammarLexer lexer = new RuleSetGrammarLexer(input); 14 | TokenStream tokens = new CommonTokenStream(lexer); 15 | RuleSetGrammarParser parser = new RuleSetGrammarParser(tokens); 16 | 17 | TreeBuilder treeBuilder = new TreeBuilder(); 18 | parser.addParseListener(treeBuilder); 19 | parser.setErrorHandler(new ExceptionThrowingErrorHandler()); 20 | 21 | parser.rule_set(); 22 | 23 | return treeBuilder.getRuleSet(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/compiler/ExceptionThrowingErrorHandler.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.compiler; 2 | 3 | import org.antlr.v4.runtime.*; 4 | 5 | /** 6 | * Error handler which throws exception on any parsing error. 7 | */ 8 | public class ExceptionThrowingErrorHandler extends DefaultErrorStrategy { 9 | @Override 10 | public void recover(Parser recognizer, RecognitionException e) { 11 | throw new RuntimeException(e); 12 | } 13 | 14 | @Override 15 | public Token recoverInline(Parser recognizer) throws RecognitionException { 16 | throw new RuntimeException(new InputMismatchException(recognizer)); 17 | } 18 | 19 | @Override 20 | public void sync(Parser recognizer) throws RecognitionException { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/compiler/TreeBuilder.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.compiler; 2 | 3 | import me.ivanyu.RuleSetGrammarBaseListener; 4 | import me.ivanyu.RuleSetGrammarParser; 5 | import me.ivanyu.pojos.*; 6 | import org.antlr.v4.runtime.misc.NotNull; 7 | 8 | import java.math.BigDecimal; 9 | import java.text.DecimalFormat; 10 | import java.text.DecimalFormatSymbols; 11 | import java.text.ParseException; 12 | import java.util.Stack; 13 | 14 | public class TreeBuilder extends RuleSetGrammarBaseListener { 15 | private RuleSet ruleSet = null; 16 | private Rule rule = null; 17 | private LogicalExpression condition = null; 18 | 19 | private Stack logicalExpressions = new Stack<>(); 20 | private Stack comparisonOperands = new Stack<>(); 21 | private Stack arithmeticExpressions = new Stack<>(); 22 | 23 | public RuleSet getRuleSet() { 24 | return ruleSet; 25 | } 26 | 27 | @Override 28 | public void enterRule_set(@NotNull RuleSetGrammarParser.Rule_setContext ctx) { 29 | assert ruleSet == null; 30 | assert rule == null; 31 | assert condition == null; 32 | 33 | assert logicalExpressions.empty(); 34 | assert comparisonOperands.empty(); 35 | assert arithmeticExpressions.empty(); 36 | 37 | this.ruleSet = new RuleSet(); 38 | } 39 | 40 | @Override 41 | public void enterSingle_rule(@NotNull RuleSetGrammarParser.Single_ruleContext ctx) { 42 | this.rule = new Rule(); 43 | } 44 | 45 | @Override 46 | public void exitConclusion(@NotNull RuleSetGrammarParser.ConclusionContext ctx) { 47 | this.rule.setConclusion(ctx.getText()); 48 | } 49 | 50 | @Override 51 | public void exitSingle_rule(@NotNull RuleSetGrammarParser.Single_ruleContext ctx) { 52 | this.rule.setCondition(this.logicalExpressions.pop()); 53 | this.ruleSet.addRule(this.rule); 54 | this.rule = null; 55 | } 56 | 57 | @Override 58 | public void exitNumericVariable(@NotNull RuleSetGrammarParser.NumericVariableContext ctx) { 59 | this.arithmeticExpressions.add(new NumericVariable(ctx.getText())); 60 | } 61 | 62 | @Override 63 | public void exitNumericConst(@NotNull RuleSetGrammarParser.NumericConstContext ctx) { 64 | DecimalFormatSymbols symbols = new DecimalFormatSymbols(); 65 | symbols.setDecimalSeparator('.'); 66 | String pattern = "#0.0#"; 67 | 68 | DecimalFormat decimalFormat = new DecimalFormat(pattern, symbols); 69 | decimalFormat.setParseBigDecimal(true); 70 | BigDecimal value; 71 | try { 72 | value = (BigDecimal)decimalFormat.parse(ctx.getText()); 73 | } catch (ParseException e) { 74 | throw new RuntimeException(e); 75 | } 76 | this.arithmeticExpressions.push(new NumericConstant(value)); 77 | } 78 | 79 | @Override 80 | public void enterNumericConst(@NotNull RuleSetGrammarParser.NumericConstContext ctx) { 81 | super.enterNumericConst(ctx); 82 | } 83 | 84 | @Override 85 | public void exitArithmeticExpressionMult(@NotNull RuleSetGrammarParser.ArithmeticExpressionMultContext ctx) { 86 | exitRealArithmeticExpression("*"); 87 | } 88 | 89 | @Override 90 | public void exitArithmeticExpressionDiv(@NotNull RuleSetGrammarParser.ArithmeticExpressionDivContext ctx) { 91 | exitRealArithmeticExpression("/"); 92 | } 93 | 94 | @Override 95 | public void exitArithmeticExpressionPlus(@NotNull RuleSetGrammarParser.ArithmeticExpressionPlusContext ctx) { 96 | exitRealArithmeticExpression("+"); 97 | } 98 | 99 | @Override 100 | public void exitArithmeticExpressionMinus(@NotNull RuleSetGrammarParser.ArithmeticExpressionMinusContext ctx) { 101 | exitRealArithmeticExpression("-"); 102 | } 103 | 104 | protected void exitRealArithmeticExpression(String op) { 105 | // popping order matters 106 | ArithmeticExpression right = this.arithmeticExpressions.pop(); 107 | ArithmeticExpression left = this.arithmeticExpressions.pop(); 108 | RealArithmeticExpression expr = new RealArithmeticExpression(op, left, right); 109 | this.arithmeticExpressions.push(expr); 110 | } 111 | 112 | @Override 113 | public void exitArithmeticExpressionNegation( 114 | @NotNull RuleSetGrammarParser.ArithmeticExpressionNegationContext ctx) { 115 | Negation negation = new Negation(this.arithmeticExpressions.pop()); 116 | this.arithmeticExpressions.push(negation); 117 | } 118 | 119 | @Override 120 | public void exitComparison_operand(@NotNull RuleSetGrammarParser.Comparison_operandContext ctx) { 121 | ArithmeticExpression expr = this.arithmeticExpressions.pop(); 122 | this.comparisonOperands.push(expr); 123 | } 124 | 125 | @Override 126 | public void exitComparisonExpressionWithOperator( 127 | @NotNull RuleSetGrammarParser.ComparisonExpressionWithOperatorContext ctx) { 128 | // popping order matters 129 | ComparisonOperand right = this.comparisonOperands.pop(); 130 | ComparisonOperand left = this.comparisonOperands.pop(); 131 | String op = ctx.getChild(1).getText(); 132 | ComparisonExpression expr = new ComparisonExpression(op, left, right); 133 | this.logicalExpressions.push(expr); 134 | } 135 | 136 | @Override 137 | public void exitLogicalConst(@NotNull RuleSetGrammarParser.LogicalConstContext ctx) { 138 | switch (ctx.getText().toUpperCase()) { 139 | case "TRUE": 140 | this.logicalExpressions.push(LogicalConstant.getTrue()); 141 | break; 142 | case "FALSE": 143 | this.logicalExpressions.push(LogicalConstant.getFalse()); 144 | break; 145 | default: 146 | throw new RuntimeException("Unknown logical constant: " + ctx.getText()); 147 | } 148 | } 149 | 150 | @Override 151 | public void exitLogicalVariable(@NotNull RuleSetGrammarParser.LogicalVariableContext ctx) { 152 | LogicalVariable variable = new LogicalVariable(ctx.getText()); 153 | this.logicalExpressions.push(variable); 154 | } 155 | 156 | @Override 157 | public void exitLogicalExpressionOr(@NotNull RuleSetGrammarParser.LogicalExpressionOrContext ctx) { 158 | // popping order matters 159 | LogicalExpression right = logicalExpressions.pop(); 160 | LogicalExpression left = logicalExpressions.pop(); 161 | OrExpression expr = new OrExpression(left, right); 162 | this.logicalExpressions.push(expr); 163 | } 164 | 165 | @Override 166 | public void exitLogicalExpressionAnd(@NotNull RuleSetGrammarParser.LogicalExpressionAndContext ctx) { 167 | // popping order matters 168 | LogicalExpression right = logicalExpressions.pop(); 169 | LogicalExpression left = logicalExpressions.pop(); 170 | AndExpression expr = new AndExpression(left, right); 171 | this.logicalExpressions.push(expr); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/AndExpression.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class AndExpression extends LogicalExpression { 6 | private final LogicalExpression left; 7 | private final LogicalExpression right; 8 | 9 | public AndExpression(LogicalExpression left, LogicalExpression right) { 10 | super("and"); 11 | 12 | this.left = left; 13 | this.right = right; 14 | } 15 | 16 | @JsonProperty("left") 17 | public LogicalExpression getLeft() { 18 | return left; 19 | } 20 | 21 | @JsonProperty("right") 22 | public LogicalExpression getRight() { 23 | return right; 24 | } 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) return true; 29 | if (o == null || getClass() != o.getClass()) return false; 30 | 31 | AndExpression other = (AndExpression) o; 32 | 33 | if (left != null ? !left.equals(other.left) : other.left != null) return false; 34 | if (right != null ? !right.equals(other.right) : other.right != null) return false; 35 | 36 | return true; 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | int result = left != null ? left.hashCode() : 0; 42 | result = 31 * result + (right != null ? right.hashCode() : 0); 43 | return result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/ArithmeticExpression.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | public interface ArithmeticExpression extends ComparisonOperand { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/ComparisonExpression.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class ComparisonExpression extends LogicalExpression { 6 | private final String operator; 7 | private final ComparisonOperand left; 8 | private final ComparisonOperand right; 9 | 10 | public ComparisonExpression(String operator, ComparisonOperand left, ComparisonOperand right) { 11 | super("comp"); 12 | this.operator = operator; 13 | this.left = left; 14 | this.right = right; 15 | } 16 | 17 | @JsonProperty("op") 18 | public String getOperator() { 19 | return operator; 20 | } 21 | 22 | @JsonProperty("left") 23 | public ComparisonOperand getLeft() { 24 | return left; 25 | } 26 | 27 | @JsonProperty("right") 28 | public ComparisonOperand getRight() { 29 | return right; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) return true; 35 | if (o == null || getClass() != o.getClass()) return false; 36 | 37 | ComparisonExpression other = (ComparisonExpression) o; 38 | 39 | if (left != null ? !left.equals(other.left) : other.left != null) return false; 40 | if (operator != null ? !operator.equals(other.operator) : other.operator != null) return false; 41 | if (right != null ? !right.equals(other.right) : other.right != null) return false; 42 | 43 | return true; 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | int result = operator != null ? operator.hashCode() : 0; 49 | result = 31 * result + (left != null ? left.hashCode() : 0); 50 | result = 31 * result + (right != null ? right.hashCode() : 0); 51 | return result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/ComparisonOperand.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | public interface ComparisonOperand { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/Conclusion.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import me.ivanyu.pojos.serializers.ConclusionSerializer; 6 | 7 | @JsonSerialize(using= ConclusionSerializer.class) 8 | public class Conclusion implements RuleSetPojo { 9 | private final String name; 10 | 11 | public Conclusion(String name) { 12 | this.name = name; 13 | } 14 | 15 | @JsonProperty 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | @Override 21 | public boolean equals(Object o) { 22 | if (this == o) return true; 23 | if (o == null || getClass() != o.getClass()) return false; 24 | 25 | Conclusion other = (Conclusion) o; 26 | 27 | if (name != null ? !name.equals(other.name) : other.name != null) return false; 28 | 29 | return true; 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | return name != null ? name.hashCode() : 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/LogicalConstant.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class LogicalConstant extends LogicalExpression { 6 | private final boolean value; 7 | 8 | private LogicalConstant(boolean value) { 9 | super("const"); 10 | 11 | this.value = value; 12 | } 13 | 14 | @JsonProperty("v") 15 | public boolean getValue() { 16 | return value; 17 | } 18 | 19 | public static LogicalConstant getTrue(){ 20 | return new LogicalConstant(true); 21 | } 22 | 23 | public static LogicalConstant getFalse(){ 24 | return new LogicalConstant(false); 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | 32 | LogicalConstant other = (LogicalConstant) o; 33 | return value == other.value; 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return (value ? 1 : 0); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/LogicalExpression.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public abstract class LogicalExpression implements RuleSetPojo { 6 | private final String type; 7 | 8 | protected LogicalExpression(String type) { 9 | this.type = type; 10 | } 11 | 12 | @JsonProperty("t") 13 | public String getType() { 14 | return type; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/LogicalVariable.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class LogicalVariable extends LogicalExpression { 6 | private final String variableName; 7 | 8 | public LogicalVariable(String variableName) { 9 | super("var"); 10 | 11 | this.variableName = variableName; 12 | } 13 | 14 | @JsonProperty("n") 15 | public String getVariableName() { 16 | return variableName; 17 | } 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) return true; 22 | if (o == null || getClass() != o.getClass()) return false; 23 | 24 | LogicalVariable other = (LogicalVariable) o; 25 | if (variableName != null 26 | ? !variableName.equals(other.variableName) 27 | : other.variableName != null) return false; 28 | 29 | return true; 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | return variableName != null ? variableName.hashCode() : 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/Negation.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class Negation implements ArithmeticExpression { 6 | private final ArithmeticExpression expression; 7 | 8 | public Negation(ArithmeticExpression expression) { 9 | this.expression = expression; 10 | } 11 | 12 | @JsonProperty("t") 13 | public String getType() { return "neg"; } 14 | 15 | @JsonProperty("expr") 16 | public ArithmeticExpression getExpression() { 17 | return expression; 18 | } 19 | 20 | @Override 21 | public boolean equals(Object o) { 22 | if (this == o) return true; 23 | if (o == null || getClass() != o.getClass()) return false; 24 | 25 | Negation other = (Negation) o; 26 | if (expression != null 27 | ? !expression.equals(other.expression) 28 | : other.expression != null) return false; 29 | 30 | return true; 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return expression != null ? expression.hashCode() : 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/NumericConstant.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.math.BigDecimal; 6 | 7 | public class NumericConstant extends NumericEntity { 8 | private final BigDecimal value; 9 | 10 | public NumericConstant(BigDecimal value) { 11 | super("const"); 12 | this.value = value; 13 | } 14 | 15 | @JsonProperty("v") 16 | public BigDecimal getValue() { 17 | return value; 18 | } 19 | 20 | @Override 21 | public boolean equals(Object o) { 22 | if (this == o) return true; 23 | if (o == null || getClass() != o.getClass()) return false; 24 | 25 | NumericConstant other = (NumericConstant) o; 26 | if (value != null ? !value.equals(other.value) : other.value != null) return false; 27 | return true; 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return value != null ? value.hashCode() : 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/NumericEntity.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public abstract class NumericEntity implements ArithmeticExpression, ComparisonOperand { 6 | private final String type; 7 | 8 | public NumericEntity(String type) { 9 | this.type = type; 10 | } 11 | 12 | @JsonProperty("t") 13 | public String getType() { 14 | return type; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/NumericVariable.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class NumericVariable extends NumericEntity { 6 | private final String variableName; 7 | 8 | public NumericVariable(String variableName) { 9 | super("var"); 10 | 11 | this.variableName = variableName; 12 | } 13 | 14 | @JsonProperty("n") 15 | public String getVariableName() { 16 | return variableName; 17 | } 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) return true; 22 | if (o == null || getClass() != o.getClass()) return false; 23 | 24 | NumericVariable other = (NumericVariable) o; 25 | if (variableName != null ? !variableName.equals(other.variableName) : other.variableName != null) 26 | return false; 27 | 28 | return true; 29 | } 30 | 31 | @Override 32 | public int hashCode() { 33 | return variableName != null ? variableName.hashCode() : 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/OrExpression.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class OrExpression extends LogicalExpression { 6 | private final LogicalExpression left; 7 | private final LogicalExpression right; 8 | 9 | public OrExpression(LogicalExpression left, LogicalExpression right) { 10 | super("or"); 11 | 12 | this.left = left; 13 | this.right = right; 14 | } 15 | 16 | @JsonProperty("left") 17 | public LogicalExpression getLeft() { 18 | return left; 19 | } 20 | 21 | @JsonProperty("right") 22 | public LogicalExpression getRight() { 23 | return right; 24 | } 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) return true; 29 | if (o == null || getClass() != o.getClass()) return false; 30 | 31 | OrExpression other = (OrExpression) o; 32 | 33 | if (left != null ? !left.equals(other.left) : other.left != null) return false; 34 | if (right != null ? !right.equals(other.right) : other.right != null) return false; 35 | 36 | return true; 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | int result = left != null ? left.hashCode() : 0; 42 | result = 31 * result + (right != null ? right.hashCode() : 0); 43 | return result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/RealArithmeticExpression.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class RealArithmeticExpression implements ArithmeticExpression { 6 | private final String operator; 7 | private final ArithmeticExpression left; 8 | private final ArithmeticExpression right; 9 | 10 | public RealArithmeticExpression(String operator, ArithmeticExpression left, ArithmeticExpression right) { 11 | this.operator = operator; 12 | this.left = left; 13 | this.right = right; 14 | } 15 | 16 | @JsonProperty("t") 17 | public String getType() { return "arith"; } 18 | 19 | @JsonProperty("op") 20 | public String getOperator() { 21 | return operator; 22 | } 23 | 24 | @JsonProperty("left") 25 | public ArithmeticExpression getLeft() { 26 | return left; 27 | } 28 | 29 | @JsonProperty("right") 30 | public ArithmeticExpression getRight() { 31 | return right; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object o) { 36 | if (this == o) return true; 37 | if (o == null || getClass() != o.getClass()) return false; 38 | 39 | RealArithmeticExpression that = (RealArithmeticExpression) o; 40 | 41 | if (left != null ? !left.equals(that.left) : that.left != null) return false; 42 | if (operator != null ? !operator.equals(that.operator) : that.operator != null) return false; 43 | if (right != null ? !right.equals(that.right) : that.right != null) return false; 44 | 45 | return true; 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | int result = operator != null ? operator.hashCode() : 0; 51 | result = 31 * result + (left != null ? left.hashCode() : 0); 52 | result = 31 * result + (right != null ? right.hashCode() : 0); 53 | return result; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/Rule.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class Rule implements RuleSetPojo { 6 | private LogicalExpression condition; 7 | 8 | private Conclusion conclusion; 9 | 10 | public Rule() { 11 | } 12 | 13 | public Rule(LogicalExpression condition, Conclusion conclusion) { 14 | this.condition = condition; 15 | this.conclusion = conclusion; 16 | } 17 | 18 | @JsonProperty("cond") 19 | public LogicalExpression getCondition() { 20 | return condition; 21 | } 22 | 23 | public void setCondition(LogicalExpression condition) { 24 | this.condition = condition; 25 | } 26 | 27 | @JsonProperty("concl") 28 | public Conclusion getConclusion() { 29 | return conclusion; 30 | } 31 | 32 | public void setConclusion(String conclusion) { 33 | this.conclusion = new Conclusion(conclusion); 34 | } 35 | 36 | @Override 37 | public boolean equals(Object o) { 38 | if (this == o) return true; 39 | if (o == null || getClass() != o.getClass()) return false; 40 | 41 | Rule rule = (Rule) o; 42 | 43 | if (conclusion != null ? !conclusion.equals(rule.conclusion) : rule.conclusion != null) return false; 44 | if (condition != null ? !condition.equals(rule.condition) : rule.condition != null) return false; 45 | 46 | return true; 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | int result = condition != null ? condition.hashCode() : 0; 52 | result = 31 * result + (conclusion != null ? conclusion.hashCode() : 0); 53 | return result; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/RuleSet.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | public class RuleSet implements RuleSetPojo { 9 | public final List rules; 10 | 11 | public RuleSet() { 12 | this.rules = new ArrayList<>(); 13 | } 14 | 15 | public RuleSet(Collection rules) { 16 | this.rules = new ArrayList<>(rules); 17 | } 18 | 19 | public List getRules() { 20 | return Collections.unmodifiableList(rules); 21 | } 22 | 23 | public void addRule(Rule rule) { 24 | this.rules.add(rule); 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | 32 | RuleSet ruleSet = (RuleSet) o; 33 | 34 | return this.rules.equals(ruleSet.rules); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return rules != null ? rules.hashCode() : 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/RuleSetPojo.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos; 2 | 3 | public interface RuleSetPojo { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/me/ivanyu/pojos/serializers/ConclusionSerializer.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu.pojos.serializers; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.JsonSerializer; 6 | import com.fasterxml.jackson.databind.SerializerProvider; 7 | import me.ivanyu.pojos.Conclusion; 8 | 9 | import java.io.IOException; 10 | 11 | public class ConclusionSerializer extends JsonSerializer { 12 | @Override 13 | public void serialize(Conclusion conclusion, 14 | JsonGenerator jsonGenerator, 15 | SerializerProvider serializerProvider) 16 | throws IOException, JsonProcessingException { 17 | jsonGenerator.writeString(conclusion.getName()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/me/ivanyu/CompilerTest.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu; 2 | 3 | import me.ivanyu.compiler.Compiler; 4 | import me.ivanyu.pojos.*; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.junit.runners.Parameterized; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.Arrays; 11 | import java.util.Collection; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | 15 | @RunWith(Parameterized.class) 16 | public class CompilerTest { 17 | private static Conclusion standardConclusion = new Conclusion("TheConclusion"); 18 | 19 | private static RuleSet createRuleSet(LogicalExpression... conditions) { 20 | RuleSet ruleSet = new RuleSet(); 21 | for (LogicalExpression condition : conditions) { 22 | Rule rule = new Rule(condition, standardConclusion); 23 | ruleSet.addRule(rule); 24 | } 25 | return ruleSet; 26 | } 27 | 28 | @Parameterized.Parameters 29 | public static Collection data() { 30 | return Arrays.asList(new Object[][]{ 31 | { 32 | "if A = 1 then TheConclusion;", 33 | createRuleSet(new ComparisonExpression("=", 34 | new NumericVariable("A"), 35 | new NumericConstant(BigDecimal.valueOf(1)))) 36 | }, 37 | 38 | { 39 | "if (true) then TheConclusion;", 40 | createRuleSet(LogicalConstant.getTrue()) 41 | }, 42 | 43 | { 44 | "if a + 2 < 1 and c or b then TheConclusion;", 45 | createRuleSet(new OrExpression( 46 | new AndExpression( 47 | new ComparisonExpression( 48 | "<", 49 | new RealArithmeticExpression( 50 | "+", 51 | new NumericVariable("a"), 52 | new NumericConstant(BigDecimal.valueOf(2))), 53 | new NumericConstant(BigDecimal.valueOf(1)) 54 | ), 55 | new LogicalVariable("c") 56 | ), 57 | new LogicalVariable("b") 58 | )) 59 | }, 60 | }); 61 | } 62 | 63 | private final String stringToCompile; 64 | private final RuleSet targetRuleSet; 65 | 66 | public CompilerTest(String stringToCompile, RuleSet targetRuleSet) { 67 | this.stringToCompile = stringToCompile; 68 | this.targetRuleSet = targetRuleSet; 69 | } 70 | 71 | @Test 72 | public void testRule() { 73 | Compiler compiler = new Compiler(); 74 | RuleSet gotRuleSet = compiler.compile(stringToCompile); 75 | assertEquals(gotRuleSet, targetRuleSet); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/me/ivanyu/GrammarTest.java: -------------------------------------------------------------------------------- 1 | package me.ivanyu; 2 | 3 | import me.ivanyu.compiler.ExceptionThrowingErrorHandler; 4 | import org.antlr.v4.runtime.ANTLRInputStream; 5 | import org.antlr.v4.runtime.CommonTokenStream; 6 | import org.antlr.v4.runtime.ParserRuleContext; 7 | import org.antlr.v4.runtime.TokenStream; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.junit.runners.Parameterized; 11 | 12 | import java.util.Arrays; 13 | import java.util.Collection; 14 | 15 | import static org.junit.Assert.assertNull; 16 | import static org.junit.Assert.fail; 17 | 18 | @RunWith(Parameterized.class) 19 | public class GrammarTest { 20 | @Parameterized.Parameters 21 | public static Collection data() { 22 | return Arrays.asList(new Object[][]{ 23 | /* Valid rules. */ 24 | { true, "if true then conclusion;" }, 25 | { true, "if false then conclusion;" }, 26 | { true, "if (true) then conclusion;" }, 27 | { true, "if (false) then conclusion;" }, 28 | 29 | { true, "if a then conclusion;" }, 30 | { true, "if (a) then conclusion;" }, 31 | { true, "if a or b then conclusion;" }, 32 | { true, "if a and b then conclusion;" }, 33 | { true, "if (a or b) and c then conclusion;" }, 34 | { true, "if a or b and c then conclusion;" }, 35 | 36 | { true, "if a_1 or b_2 and c_3_aaa then conclusion;" }, 37 | 38 | { true, "if a >= b then conclusion;" }, 39 | { true, "if a > b then conclusion;" }, 40 | { true, "if a = b then conclusion;" }, 41 | { true, "if a < b then conclusion;" }, 42 | { true, "if a <= b then conclusion;" }, 43 | { true, "if a > 0.1 then conclusion;" }, 44 | { true, "if 1.12 <= b then conclusion;" }, 45 | { true, "if 0.1 = 4 then conclusion;" }, 46 | { true, "if 5 = 5.0 then conclusion;" }, 47 | { true, "if a + 5 * b <= c / 12.0 - 1 then conclusion;" }, 48 | { true, "if (a) > (1.23) * (1 + 4) then conclusion;" }, 49 | { true, "if 1 < 4 and true then conclusion;" }, 50 | { true, "if -(a * b) < 12 and true then conclusion;" }, 51 | 52 | { true, "" }, // empty rule file 53 | { true, "// comment in a rule file" }, 54 | { true, "// comment with new line\n" }, 55 | 56 | 57 | /* Invalid rules. */ 58 | { false, "true" }, 59 | { false, "false" }, 60 | 61 | { false, "if true then conclusion" }, // No semicolon 62 | 63 | { false, "if then conclusion;" }, 64 | { false, "if true conclusion;" }, 65 | { false, "if then;" }, 66 | { false, "if;" }, 67 | { false, "if" }, 68 | { false, "then;" }, 69 | { false, "then" }, 70 | 71 | { false, "a + b" }, 72 | 73 | { false, "if a ++ b then conclusion;" }, 74 | { false, "if a + (+b) then conclusion;" }, 75 | 76 | { false, "true and false;" }, 77 | 78 | { false, "if abc $ 123 then conclusion;" }, 79 | { false, "if abc @ 123 then conclusion;" }, 80 | 81 | { false, "if aa( == 123 then conclusion;" }, 82 | { false, "if bb) == 123 then conclusion;" }, 83 | 84 | { false, "if true then conclusion1 conclusion2;" }, 85 | 86 | // Considered invalid for now 87 | { false, "if function(a, b, c) then conclusion;" }, 88 | 89 | { false, "if logical_function() then conclusion;" }, 90 | { false, "if logical_function() and (true) then conclusion;" }, 91 | 92 | { false, "if numeric_function() == 123 then conclusion;" }, 93 | { false, "if numeric_function() >= 0.12 then conclusion;" }, 94 | }); 95 | } 96 | 97 | private final boolean testValid; 98 | private final String testString; 99 | 100 | public GrammarTest(boolean testValid, String testString) { 101 | this.testValid = testValid; 102 | this.testString = testString; 103 | } 104 | 105 | @Test 106 | public void testRule() { 107 | ANTLRInputStream input = new ANTLRInputStream(this.testString); 108 | RuleSetGrammarLexer lexer = new RuleSetGrammarLexer(input); 109 | TokenStream tokens = new CommonTokenStream(lexer); 110 | 111 | RuleSetGrammarParser parser = new RuleSetGrammarParser(tokens); 112 | 113 | parser.removeErrorListeners(); 114 | parser.setErrorHandler(new ExceptionThrowingErrorHandler()); 115 | 116 | if (this.testValid) { 117 | ParserRuleContext ruleContext = parser.rule_set(); 118 | assertNull(ruleContext.exception); 119 | } else { 120 | try { 121 | ParserRuleContext ruleContext = parser.rule_set(); 122 | fail("Failed on \"" + this.testString + "\""); 123 | } catch (RuntimeException e) { 124 | // deliberately do nothing 125 | } 126 | } 127 | } 128 | } 129 | --------------------------------------------------------------------------------