I've defined a grammar, which I have to parse in C# and evaluate the resulted "constraints" on a list of dictionaries. The grammar is mainly boolean algebra.
Here you can check it:
grammar Constraint;
expr
: implyExpr
;
implyExpr
: orExpr
| leftOp=orExpr IMPLY rightOp=orExpr
;
orExpr
: andExpr
| leftOp=andExpr OR rightOp=andExpr
;
andExpr
: primaryExpr
| leftOp=primaryExpr AND rightOp=primaryExpr
;
primaryExpr
: trueExpr
| falseExpr
| parenExpr
| notParenExpr
| statement
;
trueExpr
: TRUE
;
falseExpr
: FALSE
;
parenExpr
: LPAREN expr RPAREN
;
notParenExpr
: NOT LPAREN expr RPAREN
;
statement
: eqValStatement
| notEqValStatement
| inListStatement
| notInListStatement
;
eqValStatement
: key=STR EQ value=val
;
notEqValStatement
: key=STR NEQ value=val
;
inListStatement
: key=STR EQ list=lst
;
notInListStatement
: key=STR NEQ list=lst
;
lst
: LBRACK values+=val (COMMA values+=val)* RBRACK
;
val
: strVal
| nullVal
;
strVal
: value=STR
;
nullVal
: NULL
;
// Lexer
IMPLY: '->';
OR: '|';
AND: '&';
TRUE: 'true';
FALSE: 'false';
LPAREN: '(';
RPAREN: ')';
NOT: '!';
EQ: '=';
NEQ: '!=';
LBRACK: '[';
RBRACK: ']';
COMMA: ',';
NULL: 'null';
STR: [a-zA-Z0-9-_]+;
WS: [ \t\n\r]+ -> skip;
My question is that could I use this namespace for my goal or shall I use different delegates, which would take 2 delegates as parameters and would return with a bool value according to the implemented behaviour?
I'm still at the design phase, what I've done already is the grammar definition and generating the listenerbase class for it.
I solved the problem by using delegates in the end. I generated a Predicate for each expression and combined them together. I've also integrated the recognition of the grammar into the listener class, in this way providing a simple interface for getting a predicate from an expression.
Here's the most of it:
public class ConstraintEvaluator : ConstraintBaseListener
{
public Predicate<List<ConfigurationItemHeader>>? Evaluate(string input)
{
_exprStack.Clear();
_strStack.Clear();
var stream = CharStreams.fromString(input);
ITokenSource lexer = new ConstraintLexer(stream);
ITokenStream tokens = new CommonTokenStream(lexer);
ConstraintParser parser = new(tokens);
IParseTree tree = parser.expr();
ParseTreeWalker.Default.Walk(this, tree);
return Result;
}
private Predicate<List<ConfigurationItemHeader>>? Result { get; set; }
private readonly Stack<Predicate<List<ConfigurationItemHeader>>> _exprStack = new();
private readonly Stack<string?> _strStack = new();
private static Predicate<List<ConfigurationItemHeader>> ImplicationFactory(Predicate<List<ConfigurationItemHeader>> left, Predicate<List<ConfigurationItemHeader>> right)
{
return config => !left(config) || right(config);
}
private static Predicate<List<ConfigurationItemHeader>> OrFactory(Predicate<List<ConfigurationItemHeader>> left, Predicate<List<ConfigurationItemHeader>> right)
{
return config => left(config) || right(config);
}
private static Predicate<List<ConfigurationItemHeader>> AndFactory(Predicate<List<ConfigurationItemHeader>> left, Predicate<List<ConfigurationItemHeader>> right)
{
return config => left(config) && right(config);
}
private static Predicate<List<ConfigurationItemHeader>> NotFactory(Predicate<List<ConfigurationItemHeader>> expr)
{
return config => !expr(config);
}
private static Predicate<List<ConfigurationItemHeader>> EqValFactory(string key, string? value)
{
return config =>
{
if (config.All(x => x.Key != key))
{
throw new ArgumentException(KeyNotFoundError(key));
}
return config.Find(x => x.Key == key)?.Value == value;
};
}
private static Predicate<List<ConfigurationItemHeader>> NotEqValFactory(string key, string? value)
{
return config =>
{
if (value == null)
{
return config.All(x => x.Key != key);
}
if (config.All(x => x.Key != key))
{
throw new ArgumentException(KeyNotFoundError(key));
}
return config.Find(x => x.Key == key)?.Value != value;
};
}
private static Predicate<List<ConfigurationItemHeader>> InListFactory(string key, List<string?> values)
{
return config =>
{
if (config.All(x => x.Key != key))
{
throw new ArgumentException(KeyNotFoundError(key));
}
// if x.Value is null, it means that it is a flag and the values list should contain the null value too,
// to allow the flag to be set (usually the flag shouldn't be allowed to have any other value, than null)
return values.Contains(config.Find(x => x.Key == key)?.Value);
};
}
private static Predicate<List<ConfigurationItemHeader>> NotInListFactory(string key, List<string?> values)
{
return config =>
{
if (config.All(x => x.Key != key))
{
throw new ArgumentException(KeyNotFoundError(key));
}
return !values.Contains(config.Find(x => x.Key == key)?.Value);
};
}
private static Predicate<List<ConfigurationItemHeader>> FalseFactory()
{
return _ => false;
}
private static Predicate<List<ConfigurationItemHeader>> TrueFactory()
{
return _ => true;
}
private static string StackNotEmptyError(string stackName, string methodName)
{
return $"{stackName} is not empty after {methodName}!";
}
private static string KeyNotFoundError(string key)
{
return $"Key <{key}> not found in config!";
}
public override void ExitExpr(ConstraintParser.ExprContext context)
{
Result = _exprStack.Peek();
}
public override void ExitImplyExpr(ConstraintParser.ImplyExprContext context)
{
if (context.leftOp == null || context.rightOp == null) return;
var right = _exprStack.Pop();
var left = _exprStack.Pop();
if (_exprStack.Count > 0)
{
throw new ApplicationException(StackNotEmptyError(nameof(_exprStack), nameof(ExitImplyExpr)));
}
_exprStack.Push(ImplicationFactory(left, right));
}
public override void ExitOrExpr(ConstraintParser.OrExprContext context)
{
if (context.leftOp == null || context.rightOp == null) return;
var right = _exprStack.Pop();
var left = _exprStack.Pop();
if (_exprStack.Count > 0)
{
throw new ApplicationException(StackNotEmptyError(nameof(_exprStack), nameof(ExitOrExpr)));
}
_exprStack.Push(OrFactory(left, right));
}
public override void ExitAndExpr(ConstraintParser.AndExprContext context)
{
if (context.leftOp == null || context.rightOp == null) return;
var right = _exprStack.Pop();
var left = _exprStack.Pop();
if (_exprStack.Count > 0)
{
throw new ApplicationException(StackNotEmptyError(nameof(_exprStack), nameof(ExitAndExpr)));
}
_exprStack.Push(AndFactory(left, right));
}
public override void ExitTrueExpr(ConstraintParser.TrueExprContext context)
{
_exprStack.Push(TrueFactory());
}
public override void ExitFalseExpr(ConstraintParser.FalseExprContext context)
{
_exprStack.Push(FalseFactory());
}
public override void ExitNotParenExpr(ConstraintParser.NotParenExprContext context)
{
var expr = _exprStack.Pop();
if (_exprStack.Count > 0)
{
throw new ApplicationException(StackNotEmptyError(nameof(_exprStack), nameof(ExitNotParenExpr)));
}
_exprStack.Push(NotFactory(expr));
}
public override void ExitEqValStatement(ConstraintParser.EqValStatementContext context)
{
var key = context.key.Text;
var value = _strStack.Pop();
if (_strStack.Count > 0)
{
throw new ApplicationException(StackNotEmptyError(nameof(_strStack), nameof(ExitEqValStatement)));
}
_exprStack.Push(EqValFactory(key, value));
}
public override void ExitNotEqValStatement(ConstraintParser.NotEqValStatementContext context)
{
var key = context.key.Text;
var value = _strStack.Pop();
if (_strStack.Count > 0)
{
throw new ApplicationException(StackNotEmptyError(nameof(_strStack),nameof(ExitNotEqValStatement)));
}
_exprStack.Push(NotEqValFactory(key, value));
}
public override void ExitInListStatement(ConstraintParser.InListStatementContext context)
{
var key = context.key.Text;
var values = _strStack.ToList();
_strStack.Clear();
_exprStack.Push(InListFactory(key, values));
}
public override void ExitNotInListStatement(ConstraintParser.NotInListStatementContext context)
{
var key = context.key.Text;
var values = _strStack.ToList();
_strStack.Clear();
_exprStack.Push(NotInListFactory(key, values));
}
public override void ExitStrVal(ConstraintParser.StrValContext context)
{
_strStack.Push(context.value.Text);
}
public override void ExitNullVal(ConstraintParser.NullValContext context)
{
_strStack.Push(null);
}
}
In the end I used List of ConfigurationItems, which have Key and Value properties, bc. of the domain requirements of my application.