using System.Collections.Generic;
using System.Text.RegularExpressions;
public class ExpressionEvaluator
private List<double> _vars = [];
public static double Evaluate(string strExpr)
var expr = new ExpressionEvaluator(strExpr.Replace('\n', ' '));
double value = expr.ParseExpression();
private static double ResolveVariable(string name, List<string> args)
Console.Write($"Resolving variable: <{name}{(args.Count > 0 ? ":" + string.Join(":", args) : "")}>");
Console.WriteLine($" == {value}");
private ExpressionEvaluator(string expr)
string pattern = @"<([^= \t\n0-9.<>()!-][^<>\n]*|[ \t]+(?:[^ \t\n0-9.<>()!-][^<>\n]*)?)>";
while (Regex.IsMatch(expr, pattern))
expr = Regex.Replace(expr, pattern, match =>
string[] parts = match.Groups[1].Value.Split(':');
string name = parts[0].Trim();
List<string> args = new List<string>();
for (int i = 1; i < parts.Length; i++)
args.Add(parts[i].Trim());
_vars.Add(ResolveVariable(name, args));
_expr = string.Join(" ", expr.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries));
Console.WriteLine($"Resolved: {_expr.Replace("\n", "<...>")}");
private char Current => _pos < _expr.Length ? _expr[_pos] : '\0';
private string RemainDiagnosis => _expr.Substring(_pos).Replace("\n", "<...>");
private void Next() => _pos++;
private void CheckNoRemain()
throw new Exception($"Unexpected: {RemainDiagnosis}");
private bool Match(char expected, bool skipSpaces = true)
if (Current == ' ' && skipSpaces)
private double ParseExpression() => ParseOr();
double left = ParseXor();
bool bLeft = ToBool(left);
bool bRight = ToBool(ParseXor());
Console.Write($"Or({bLeft}, {bRight})");
left = (bLeft || bRight) ? 1 : 0;
Console.WriteLine($" == {left}");
private double ParseXor()
double left = ParseAnd();
bool bLeft = ToBool(left);
bool bRight = ToBool(ParseAnd());
Console.Write($"Xor({bLeft}, {bRight})");
left = (bLeft ^ bRight) ? 1 : 0;
Console.WriteLine($" == {left}");
private double ParseAnd()
double left = ParseEquality();
bool bLeft = ToBool(left);
bool bRight = ToBool(ParseEquality());
Console.Write($"And({bLeft}, {bRight})");
left = (bLeft && bRight) ? 1 : 0;
Console.WriteLine($" == {left}");
private double ParseEquality()
double left = ParseComparison();
if (!Match('=', skipSpaces: false))
throw new Exception("Expected '=='");
double right = ParseComparison();
Console.Write($"Equal({left}, {right})");
left = right == left ? 1 : 0;
Console.WriteLine($" == {left}");
if (!Match('=', skipSpaces: false))
throw new Exception("Expected '!='");
double right = ParseComparison();
Console.Write($"NotEqual({left}, {right})");
left = right != left ? 1 : 0;
Console.WriteLine($" == {left}");
private double ParseComparison()
double left = ParseAddSub();
if (Match('=', skipSpaces: false)) {
double right = ParseAddSub();
Console.Write($"AboveEqual({left}, {right})");
left = left >= right ? 1 : 0;
double right = ParseAddSub();
Console.Write($"Above({left}, {right})");
left = left > right ? 1 : 0;
Console.WriteLine($" == {left}");
if (Match('=', skipSpaces: false)) {
double right = ParseAddSub();
Console.Write($"BelowEqual({left}, {right})");
left = left <= right ? 1 : 0;
double right = ParseAddSub();
Console.Write($"Below({left}, {right})");
left = left < right ? 1 : 0;
Console.WriteLine($" == {left}");
private double ParseAddSub()
double left = ParseMulDiv();
double right = ParseMulDiv();
Console.Write($"Add({left}, {right})");
Console.WriteLine($" == {left}");
double right = ParseMulDiv();
Console.Write($"Sub({left}, {right})");
Console.WriteLine($" == {left}");
private double ParseMulDiv()
double left = ParseUnary();
double right = ParseUnary();
Console.Write($"Mul({left}, {right})");
Console.WriteLine($" == {left}");
double right = ParseUnary();
Console.Write($"Div({left}, {right})");
Console.WriteLine($" == {left}");
private double ParseUnary()
double left = ParseUnary();
Console.WriteLine($"Neg({left})");
Console.WriteLine($" == {left}");
bool bLeft = ToBool(ParseUnary());
Console.Write($"Not({bLeft})");
double left = !bLeft ? 1 : 0;
Console.WriteLine($" == {left}");
return ParseFunctionCall();
private double ParseFunctionCall()
double left = ParsePrimary();
var args = new List<double>();
args.Add(ParseExpression());
if (Match(':')) continue;
throw new Exception($"Unexpected: {RemainDiagnosis}");
left = DoFunc(left, args);
private double ParsePrimary()
double val = ParseExpression();
if (!Match(')')) throw new Exception($"Expected ')', got {RemainDiagnosis}");
Console.Write($"Number(Vars[{_ivar}])");
left = (_ivar < _vars.Count) ? _vars[_ivar++] : 0;
while (char.IsDigit(Current) || Current == '.') Next();
throw new Exception($"Unexpected end of input");
throw new Exception($"Unexpected: {RemainDiagnosis}");
string strLeft = _expr.Substring(start, _pos - start);
Console.Write($"Number(\"{strLeft}\")");
left = double.Parse(strLeft);
Console.WriteLine($" == {left}");
private bool ToBool(double value)
if (value != 0 && value != 1)
Console.WriteLine($"Warning: treating non-zero logical value {value} as 1");
Console.Write($"Bool({value})");
Console.WriteLine($" == {bLeft}");
private double DoFunc(double idxFunc, List<double> args)
string strArgList = string.Join(", ", args.Select(x => x.ToString()).ToList());
Console.WriteLine($"Function call: Function({idxFunc})({strArgList})");
public static void Main()
(bool NoError, double Value) Exec(string expr)
Console.WriteLine($"\nExpr: {expr}");
double result = Evaluate(expr);
Console.WriteLine($"Result: {result}");
Console.WriteLine($"Error: {ex.Message}");
void Expect(string expr, double value)
if (Exec(expr) != (true, value)) {
Console.WriteLine($"Wrong: Expected {value}");
bool Bool(double v) => v != 0;
double Double(bool v) => v ? 1 : 0;
void ExpectBool(string expr, bool value) => Expect(expr, Double(value));
void ExpectError(string expr)
if (Exec(expr).NoError) {
Console.WriteLine($"Wrong: Expected error");
ExpectBool("((3 + 4) * 2 > 10) & ((8 / 2) == 4)", ((3 + 4) * 2 > 10) && ((8 / 2) == 4));
ExpectBool("(5 * (2 + 3) != 20) | ((1 + 1) * 2 == 4)", (5 * (2 + 3) != 20) || ((1 + 1) * 2 == 4));
ExpectBool("!((3 + 2) < 4)", !((3 + 2) < 4));
ExpectBool("!(2 + 2 == 4) | (3 ^ 3)", !(2 + 2 == 4) || (Bool(3) ^ Bool(3)));
ExpectBool("(((1+2)*3)>8)^((4*2)<=8)", (((1+2)*3)>8)^((4*2)<=8));
ExpectBool("(<a:val> + 3) * 2 > (<b:x:y> - 1)", (x + 3) * 2 > (x - 1));
ExpectBool("((<x> + 2) * 5) == (10 + <x>)", ((x + 2) * 5) == (10 + x));
Expect("5 + (3 * (<temp> + 2))", 5 + (3 * (x + 2)));
ExpectBool("!(1 & 0) | (0 ^ 1)", !(Bool(1) & Bool(0)) || (Bool(0) ^ Bool(1)));
ExpectBool("!1 & 1 | 1", !Bool(1) && Bool(1) || Bool(1));
ExpectBool("!(1 | 0) & (1 ^ 0)", !(Bool(1) || Bool(0)) && (Bool(1) ^ Bool(0)));
ExpectBool("10 / (2 + 3) >= 2", 10 / (2 + 3) >= 2);
ExpectBool("1 + 2 + 3 + 4 + 5 == 15", 1 + 2 + 3 + 4 + 5 == 15);
ExpectBool("((2+3)*2 + 1) == (3*3 + 1)", ((2+3)*2 + 1) == (3*3 + 1));
ExpectBool("4 * (2 + (1 + 3)) > 20", 4 * (2 + (1 + 3)) > 20);
ExpectBool("((<a:x:y> * 2 + 1) >= 1) & (3 < 4)", ((x * 2 + 1) >= 1) && (3 < 4));
ExpectBool("<foo:bar:baz> + 1 != 1", x + 1 != 1);
Expect("<math:atan2>(<math:cos>(<lc:t>) * <math:pi> : <math:sin>(<lc:t>) * <math:pi>)", f);
Expect("<func:select>(<math:get_random_int>(0 : 1) : <math:sin> : <math:cos>)(<lc:t>)", f);
Expect("(<game:set_effect_attr> + <math:get_random_int>(0 : <game:effect_attr_count>))(42)", f);
Expect("1.-0.7", 1.0-0.7);
ExpectBool("\t40\t+\t2\t==\t42\t", 40 + 2 == 42);
ExpectBool(" < jp > < < lc : perfect_count > < ( < jb > < < lc : bad_count > ) ", Double(x < x) < Double(x < x));
ExpectBool(" 3 < < lc : perfect_count > < ( 1 < < lc : bad_count > ) ", Double(3 < x) < Double(1 < x));
ExpectBool("2 <4&4> 2", 2 <4&&4> 2);
ExpectBool("2 <=4&4>= 2", 2 <=4&4>= 2);
ExpectBool("0 <=1> 0", Double(0 <=1)> 0);
Expect("42(0(69) : 11(45))", f);
Expect("<:simplestyleSweat:>", x);
ExpectBool("3 < < x & x > > 3", Double(3 < x) > 3);
ExpectError("3 < < 4 & 4 > > 3");
ExpectError("<artist:Tanger>(:3)");
ExpectError("<math:max>(3 0 0)");
ExpectError("helloworld");
Console.WriteLine($"\n{nFail} cases failed!");
Console.WriteLine($"\nPass!");