424 lines
14 KiB
C#
424 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UniLinq;
|
|
using Sprache;
|
|
using static Sprache.Parse;
|
|
|
|
namespace CodeWriter.ExpressionParser
|
|
{
|
|
public delegate T Expression<out T>();
|
|
|
|
public abstract class ExpressionParser<T>
|
|
{
|
|
private readonly Dictionary<string, ExprBuilder> _builderCached = new Dictionary<string, ExprBuilder>();
|
|
private Parser<ExprBuilder> _parserCached;
|
|
|
|
public Expression<bool> CompilePredicate(string input, ExpressionContext<T> context, bool cache)
|
|
{
|
|
var expr = Compile(input, context, cache);
|
|
return () => IsTrue(expr.Invoke());
|
|
}
|
|
|
|
public Expression<T> Compile(string input, ExpressionContext<T> context, bool cache)
|
|
{
|
|
context = context ?? new ExpressionContext<T>();
|
|
|
|
_parserCached = _parserCached ?? CreateParser();
|
|
|
|
ExprBuilder builder;
|
|
try
|
|
{
|
|
if (cache)
|
|
{
|
|
if (!_builderCached.TryGetValue(input, out builder))
|
|
{
|
|
builder = _parserCached.Parse(input);
|
|
_builderCached.Add(input, builder);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
builder = _parserCached.Parse(input);
|
|
}
|
|
}
|
|
catch (ParseException parseException)
|
|
{
|
|
throw new ExpressionParseException(input, parseException);
|
|
}
|
|
|
|
return builder.Invoke(context);
|
|
}
|
|
|
|
private static Parser<BinaryFunc> Operator(string op, BinaryFunc fun)
|
|
{
|
|
return String(op).Token().Return(fun).Named(op);
|
|
}
|
|
|
|
private static Parser<ExprBuilder> BinaryOperator(Parser<BinaryFunc> op,
|
|
Parser<ExprBuilder> operand, Func<BinaryFunc, ExprBuilder, ExprBuilder, ExprBuilder> apply) =>
|
|
(
|
|
from l in operand
|
|
from rest in
|
|
(
|
|
from f in op
|
|
from r in operand
|
|
select apply(f, l, r)
|
|
).Optional()
|
|
select rest.IsEmpty ? l : rest.Get()
|
|
);
|
|
|
|
private Parser<ExprBuilder> CreateParser()
|
|
{
|
|
var letterOrUnderscore = Char(c => char.IsLetter(c) || c == '_',
|
|
"letter or underscore");
|
|
var letterOrDigitOrUnderscore = Char(c => char.IsLetterOrDigit(c) || c == '_',
|
|
"letter or digit or underscore");
|
|
|
|
var constant = (
|
|
from number in DecimalInvariant
|
|
select MakeConstant(number, Parse)
|
|
).Named("number");
|
|
|
|
var add = Operator("+", Add);
|
|
var sub = Operator("-", Sub);
|
|
var mul = Operator("*", Mul);
|
|
var div = Operator("/", Div);
|
|
var mod = Operator("%", Mod);
|
|
var pow = Operator("^", Pow);
|
|
var eq = Operator("=", Equal);
|
|
var neq = Operator("!=", NotEqual);
|
|
var and = Operator("AND", And);
|
|
var or = Operator("OR", Or);
|
|
var lt = Operator("<", LessThan);
|
|
var lte = Operator("<=", LessThanOrEqual);
|
|
var gt = Operator(">", GreaterThan);
|
|
var gte = Operator(">=", GreaterThanOrEqual);
|
|
|
|
var variable =
|
|
(
|
|
from nameHead in letterOrUnderscore.Once().Text()
|
|
from nameTail in letterOrDigitOrUnderscore.Many().Text()
|
|
select MakeVariable(nameHead + nameTail)
|
|
).Named("variable");
|
|
|
|
Parser<ExprBuilder> expression = null;
|
|
|
|
var function =
|
|
(
|
|
from name in Letter.AtLeastOnce().Text()
|
|
from lparen in Char('(')
|
|
// ReSharper disable once AccessToModifiedClosure
|
|
from expr in Ref(() => expression).DelimitedBy(Char(',').Token())
|
|
from rparen in Char(')')
|
|
select MakeFunction(name, expr.ToList())
|
|
).Named("function");
|
|
|
|
var factor =
|
|
(
|
|
from lparen in Char('(')
|
|
// ReSharper disable once AccessToModifiedClosure
|
|
from expr in Ref(() => expression)
|
|
from rparen in Char(')')
|
|
select expr
|
|
).Named("expression")
|
|
.XOr(constant)
|
|
.XOr(function)
|
|
.Or(variable);
|
|
|
|
var operand =
|
|
(
|
|
(
|
|
from sign in Char('-')
|
|
from fact in factor
|
|
select MakeUnary(Negate, fact)
|
|
)
|
|
.XOr(factor)
|
|
).Token();
|
|
|
|
var term1 = ChainRightOperator(pow, operand, MakeBinary);
|
|
var term2 = ChainOperator(mul.Or(div).Or(mod), term1, MakeBinary);
|
|
var term3 = ChainOperator(add.Or(sub), term2, MakeBinary);
|
|
var term4 = BinaryOperator(lte.Or(lt).Or(gte).Or(gt), term3, MakeBinary);
|
|
var term5 = BinaryOperator(eq.Or(neq), term4, MakeBinary);
|
|
var term6 = ChainOperator(and, term5, MakeBinary);
|
|
var term7 = ChainOperator(or, term6, MakeBinary);
|
|
|
|
expression = term7;
|
|
|
|
return expression.End();
|
|
}
|
|
|
|
protected abstract T False { get; }
|
|
protected abstract T True { get; }
|
|
|
|
protected abstract T Parse(string input);
|
|
protected abstract T Negate(T v);
|
|
protected abstract T Add(T a, T b);
|
|
protected abstract T Sub(T a, T b);
|
|
protected abstract T Mul(T a, T b);
|
|
protected abstract T Div(T a, T b);
|
|
protected abstract T Mod(T a, T b);
|
|
protected abstract T Pow(T a, T b);
|
|
protected abstract T Equal(T a, T b);
|
|
protected abstract T NotEqual(T a, T b);
|
|
protected abstract T LessThan(T a, T b);
|
|
protected abstract T LessThanOrEqual(T a, T b);
|
|
protected abstract T GreaterThan(T a, T b);
|
|
protected abstract T GreaterThanOrEqual(T a, T b);
|
|
protected abstract bool IsTrue(T v);
|
|
|
|
protected abstract T Round(T v);
|
|
protected abstract T Floor(T v);
|
|
protected abstract T Ceiling(T v);
|
|
protected abstract T Log10(T v);
|
|
|
|
protected virtual T Log(T v, T newBase) => Div(Log10(v), Log10(newBase));
|
|
|
|
private T Not(T v) => IsTrue(v) ? False : True;
|
|
private T And(T a, T b) => IsTrue(a) ? b : a;
|
|
private T Or(T a, T b) => IsTrue(a) ? a : b;
|
|
private T Min(T a, T b) => IsTrue(GreaterThan(a, b)) ? b : a;
|
|
private T Max(T a, T b) => IsTrue(GreaterThan(b, a)) ? b : a;
|
|
|
|
private delegate Expression<T> ExprBuilder(ExpressionContext<T> context);
|
|
|
|
private delegate T UnaryFunc(T a);
|
|
|
|
private delegate T BinaryFunc(T a, T b);
|
|
|
|
private ExprBuilder MakeFunction(string name, List<ExprBuilder> parameterBuilders)
|
|
{
|
|
switch (name)
|
|
{
|
|
case "NOT":
|
|
return MakeFunction1(Not);
|
|
|
|
case "ROUND":
|
|
return MakeFunction1(Round);
|
|
|
|
case "CEILING":
|
|
return MakeFunction1(Ceiling);
|
|
|
|
case "FLOOR":
|
|
return MakeFunction1(Floor);
|
|
|
|
case "LOG":
|
|
return parameterBuilders.Count == 2
|
|
? MakeBinary(Log, parameterBuilders[0], parameterBuilders[1])
|
|
: MakeFunction1(Log10);
|
|
|
|
case "MIN":
|
|
return MakeFunctionFold(Min);
|
|
|
|
case "MAX":
|
|
return MakeFunctionFold(Max);
|
|
|
|
case "IF":
|
|
if (parameterBuilders.Count < 3 ||
|
|
parameterBuilders.Count % 2 != 1)
|
|
{
|
|
throw new FunctionNotDefinedException(name, "Wrong parameters count");
|
|
}
|
|
|
|
return context =>
|
|
{
|
|
var conditions = new List<Expression<T>>();
|
|
var results = new List<Expression<T>>();
|
|
var defaultResult = parameterBuilders[parameterBuilders.Count - 1].Invoke(context);
|
|
|
|
for (var i = 0; i < parameterBuilders.Count - 1; i += 2)
|
|
{
|
|
conditions.Add(parameterBuilders[i].Invoke(context));
|
|
results.Add(parameterBuilders[i + 1].Invoke(context));
|
|
}
|
|
|
|
return () =>
|
|
{
|
|
for (var i = 0; i < conditions.Count; i++)
|
|
{
|
|
if (IsTrue(conditions[i].Invoke()))
|
|
{
|
|
return results[i].Invoke();
|
|
}
|
|
}
|
|
|
|
return defaultResult.Invoke();
|
|
};
|
|
};
|
|
|
|
default: throw new FunctionNotDefinedException(name, "Unknown name");
|
|
}
|
|
|
|
ExprBuilder MakeFunction1(Func<T, T> func)
|
|
{
|
|
if (parameterBuilders.Count != 1)
|
|
{
|
|
throw new FunctionNotDefinedException(name, "Wrong parameters count");
|
|
}
|
|
|
|
return context =>
|
|
{
|
|
var inner = parameterBuilders[0].Invoke(context);
|
|
return () => func(inner.Invoke());
|
|
};
|
|
}
|
|
|
|
ExprBuilder MakeFunctionFold(Func<T, T, T> func)
|
|
{
|
|
if (parameterBuilders.Count < 1)
|
|
{
|
|
throw new FunctionNotDefinedException(name, "Wrong parameters count");
|
|
}
|
|
|
|
return context =>
|
|
{
|
|
var inner = new List<Expression<T>>();
|
|
|
|
for (var i = 0; i < parameterBuilders.Count; i++)
|
|
{
|
|
inner.Add(parameterBuilders[i].Invoke(context));
|
|
}
|
|
|
|
return () =>
|
|
{
|
|
var result = inner[0].Invoke();
|
|
for (var i = 1; i < inner.Count; i++)
|
|
{
|
|
result = func(result, inner[i].Invoke());
|
|
}
|
|
|
|
return result;
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
private static ExprBuilder MakeUnary(UnaryFunc func, ExprBuilder innerBuilder)
|
|
{
|
|
return context =>
|
|
{
|
|
var inner = innerBuilder.Invoke(context);
|
|
return () => func(inner.Invoke());
|
|
};
|
|
}
|
|
|
|
private static ExprBuilder MakeBinary(BinaryFunc func, ExprBuilder l, ExprBuilder r)
|
|
{
|
|
return context =>
|
|
{
|
|
var left = l.Invoke(context);
|
|
var right = r.Invoke(context);
|
|
return () => func(left.Invoke(), right.Invoke());
|
|
};
|
|
}
|
|
|
|
private ExprBuilder MakeVariable(string name)
|
|
{
|
|
if (name.Equals("TRUE", StringComparison.Ordinal))
|
|
{
|
|
return context => () => True;
|
|
}
|
|
|
|
if (name.Equals("FALSE", StringComparison.Ordinal))
|
|
{
|
|
return context => () => False;
|
|
}
|
|
|
|
return context =>
|
|
{
|
|
var variable = context.GetVariable(name);
|
|
return variable;
|
|
};
|
|
}
|
|
|
|
private static ExprBuilder MakeConstant(string valueString, Func<string, T> parser)
|
|
{
|
|
var value = parser(valueString);
|
|
return context => () => value;
|
|
}
|
|
}
|
|
|
|
public class ExpressionContext<T>
|
|
{
|
|
private readonly ExpressionContext<T> _parent;
|
|
private readonly Func<string, Expression<T>> _unregisteredVariableResolver;
|
|
|
|
private readonly Dictionary<string, Expression<T>> _variables = new Dictionary<string, Expression<T>>();
|
|
|
|
public ExpressionContext(ExpressionContext<T> parent = null,
|
|
Func<string, Expression<T>> unregisteredVariableResolver = null)
|
|
{
|
|
_parent = parent;
|
|
_unregisteredVariableResolver = unregisteredVariableResolver;
|
|
}
|
|
|
|
public void RegisterVariable(string name, Expression<T> value)
|
|
{
|
|
if (_variables.ContainsKey(name))
|
|
{
|
|
throw new InvalidOperationException($"Variable {name} already registered");
|
|
}
|
|
|
|
_variables.Add(name, value);
|
|
}
|
|
|
|
public Expression<T> GetVariable(string name, bool nullIsOk = false)
|
|
{
|
|
if (_variables.TryGetValue(name, out var variable))
|
|
{
|
|
return variable;
|
|
}
|
|
|
|
if (_unregisteredVariableResolver != null)
|
|
{
|
|
variable = _unregisteredVariableResolver.Invoke(name);
|
|
if (variable != null)
|
|
{
|
|
return variable;
|
|
}
|
|
}
|
|
|
|
var parentVariable = _parent?.GetVariable(name, nullIsOk: true);
|
|
if (parentVariable != null)
|
|
{
|
|
return parentVariable;
|
|
}
|
|
|
|
if (nullIsOk)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
throw new VariableNotDefinedException(name);
|
|
}
|
|
}
|
|
|
|
[Obsolete("ExpresionContext contains a typo. Use ExpressionContext instead", true)]
|
|
public class ExpresionContext<T> : ExpressionContext<T>
|
|
{
|
|
}
|
|
|
|
public class VariableNotDefinedException : Exception
|
|
{
|
|
public VariableNotDefinedException(string name)
|
|
: base($"Variable '{name}' not defined")
|
|
{
|
|
}
|
|
}
|
|
|
|
public class FunctionNotDefinedException : Exception
|
|
{
|
|
public FunctionNotDefinedException(string name, string reason) : base(
|
|
$"Function '{name}' not defined: {reason}")
|
|
{
|
|
}
|
|
}
|
|
|
|
public class ExpressionParseException : Exception
|
|
{
|
|
public ExpressionParseException(string expression, ParseException parseException)
|
|
: base($"Failed to parse expression '{expression}'{Environment.NewLine}{parseException.Message}")
|
|
{
|
|
}
|
|
}
|
|
} |