Add formula engine: data-driven expression evaluator (Core)
CI / build-test (push) Successful in 1m19s
CI / build-test (push) Successful in 1m19s
Keystone for the gene system. A Formula compiles a string from a def (lexer -> recursive-descent parser -> tree of Func<IFormulaContext,float>) once, then evaluates allocation-free against a variable context. Supports + - * / %, comparisons, && || !, ternary, the constants pi/tau/e, and functions abs sign floor ceil round sqrt exp log sin cos tan min max pow clamp lerp step. Deterministic and side-effect-free so gene-effect formulas stay pure. Covered by FormulaTests (parsing, precedence, functions, logic/ternary, variables, errors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2ac074004a
commit
10898b08a0
@@ -0,0 +1,217 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace MrGameEng.Formulas;
|
||||
|
||||
internal enum TokenType
|
||||
{
|
||||
Number,
|
||||
Identifier,
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
Percent,
|
||||
LParen,
|
||||
RParen,
|
||||
Comma,
|
||||
Less,
|
||||
Greater,
|
||||
LessEqual,
|
||||
GreaterEqual,
|
||||
EqualEqual,
|
||||
NotEqual,
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
Question,
|
||||
Colon,
|
||||
End,
|
||||
}
|
||||
|
||||
internal readonly struct Token(TokenType type, int position, float number = 0f, string text = "")
|
||||
{
|
||||
public TokenType Type { get; } = type;
|
||||
public int Position { get; } = position;
|
||||
public float Number { get; } = number;
|
||||
public string Text { get; } = text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns a formula string into a flat token list. Pure and allocation-light; recognizes numbers,
|
||||
/// identifiers, the arithmetic/comparison/logical operators and the punctuation the parser needs.
|
||||
/// </summary>
|
||||
internal static class FormulaLexer
|
||||
{
|
||||
public static List<Token> Tokenize(string source)
|
||||
{
|
||||
var tokens = new List<Token>();
|
||||
var i = 0;
|
||||
while (i < source.Length)
|
||||
{
|
||||
var c = source[i];
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
char.IsDigit(c)
|
||||
|| (c == '.' && i + 1 < source.Length && char.IsDigit(source[i + 1]))
|
||||
)
|
||||
{
|
||||
var start = i;
|
||||
while (i < source.Length && (char.IsDigit(source[i]) || source[i] == '.'))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
var span = source.AsSpan(start, i - start);
|
||||
if (
|
||||
!float.TryParse(
|
||||
span,
|
||||
NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var value
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new FormulaException(
|
||||
$"Invalid number '{span.ToString()}' at position {start}."
|
||||
);
|
||||
}
|
||||
|
||||
tokens.Add(new Token(TokenType.Number, start, value));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char.IsLetter(c) || c == '_')
|
||||
{
|
||||
var start = i;
|
||||
while (i < source.Length && (char.IsLetterOrDigit(source[i]) || source[i] == '_'))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
tokens.Add(
|
||||
new Token(TokenType.Identifier, start, text: source.Substring(start, i - start))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var pos = i;
|
||||
switch (c)
|
||||
{
|
||||
case '+':
|
||||
tokens.Add(new Token(TokenType.Plus, pos));
|
||||
i++;
|
||||
break;
|
||||
case '-':
|
||||
tokens.Add(new Token(TokenType.Minus, pos));
|
||||
i++;
|
||||
break;
|
||||
case '*':
|
||||
tokens.Add(new Token(TokenType.Star, pos));
|
||||
i++;
|
||||
break;
|
||||
case '/':
|
||||
tokens.Add(new Token(TokenType.Slash, pos));
|
||||
i++;
|
||||
break;
|
||||
case '%':
|
||||
tokens.Add(new Token(TokenType.Percent, pos));
|
||||
i++;
|
||||
break;
|
||||
case '(':
|
||||
tokens.Add(new Token(TokenType.LParen, pos));
|
||||
i++;
|
||||
break;
|
||||
case ')':
|
||||
tokens.Add(new Token(TokenType.RParen, pos));
|
||||
i++;
|
||||
break;
|
||||
case ',':
|
||||
tokens.Add(new Token(TokenType.Comma, pos));
|
||||
i++;
|
||||
break;
|
||||
case '?':
|
||||
tokens.Add(new Token(TokenType.Question, pos));
|
||||
i++;
|
||||
break;
|
||||
case ':':
|
||||
tokens.Add(new Token(TokenType.Colon, pos));
|
||||
i++;
|
||||
break;
|
||||
case '<':
|
||||
i = AddMaybeEqual(tokens, source, i, TokenType.LessEqual, TokenType.Less);
|
||||
break;
|
||||
case '>':
|
||||
i = AddMaybeEqual(tokens, source, i, TokenType.GreaterEqual, TokenType.Greater);
|
||||
break;
|
||||
case '=':
|
||||
if (Next(source, i) == '=')
|
||||
{
|
||||
tokens.Add(new Token(TokenType.EqualEqual, pos));
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
|
||||
throw new FormulaException($"Expected '==' at position {pos}.");
|
||||
case '!':
|
||||
if (Next(source, i) == '=')
|
||||
{
|
||||
tokens.Add(new Token(TokenType.NotEqual, pos));
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
|
||||
tokens.Add(new Token(TokenType.Not, pos));
|
||||
i++;
|
||||
break;
|
||||
case '&':
|
||||
if (Next(source, i) == '&')
|
||||
{
|
||||
tokens.Add(new Token(TokenType.And, pos));
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
|
||||
throw new FormulaException($"Expected '&&' at position {pos}.");
|
||||
case '|':
|
||||
if (Next(source, i) == '|')
|
||||
{
|
||||
tokens.Add(new Token(TokenType.Or, pos));
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
|
||||
throw new FormulaException($"Expected '||' at position {pos}.");
|
||||
default:
|
||||
throw new FormulaException($"Unexpected character '{c}' at position {pos}.");
|
||||
}
|
||||
}
|
||||
|
||||
tokens.Add(new Token(TokenType.End, source.Length));
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static int AddMaybeEqual(
|
||||
List<Token> tokens,
|
||||
string source,
|
||||
int i,
|
||||
TokenType withEqual,
|
||||
TokenType plain
|
||||
)
|
||||
{
|
||||
if (Next(source, i) == '=')
|
||||
{
|
||||
tokens.Add(new Token(withEqual, i));
|
||||
return i + 2;
|
||||
}
|
||||
|
||||
tokens.Add(new Token(plain, i));
|
||||
return i + 1;
|
||||
}
|
||||
|
||||
private static char Next(string source, int i) => i + 1 < source.Length ? source[i + 1] : '\0';
|
||||
}
|
||||
Reference in New Issue
Block a user