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,90 @@
|
||||
using Node = System.Func<MrGameEng.Formulas.IFormulaContext, float>;
|
||||
|
||||
namespace MrGameEng.Formulas;
|
||||
|
||||
/// <summary>
|
||||
/// The built-in function set available inside formulas. Each entry validates its argument count at
|
||||
/// compile time and returns a <see cref="Node"/> that evaluates its operands then the math. Kept
|
||||
/// deterministic and side-effect-free so formulas stay pure.
|
||||
/// </summary>
|
||||
internal static class FormulaFunctions
|
||||
{
|
||||
public static Node Build(string name, List<Node> args)
|
||||
{
|
||||
switch (name)
|
||||
{
|
||||
case "abs":
|
||||
return Unary(name, args, MathF.Abs);
|
||||
case "sign":
|
||||
return Unary(name, args, x => MathF.Sign(x));
|
||||
case "floor":
|
||||
return Unary(name, args, MathF.Floor);
|
||||
case "ceil":
|
||||
return Unary(name, args, MathF.Ceiling);
|
||||
case "round":
|
||||
return Unary(name, args, MathF.Round);
|
||||
case "sqrt":
|
||||
return Unary(name, args, MathF.Sqrt);
|
||||
case "exp":
|
||||
return Unary(name, args, MathF.Exp);
|
||||
case "log":
|
||||
return args.Count == 2
|
||||
? Binary(name, args, MathF.Log)
|
||||
: Unary(name, args, MathF.Log);
|
||||
case "sin":
|
||||
return Unary(name, args, MathF.Sin);
|
||||
case "cos":
|
||||
return Unary(name, args, MathF.Cos);
|
||||
case "tan":
|
||||
return Unary(name, args, MathF.Tan);
|
||||
case "min":
|
||||
return Binary(name, args, MathF.Min);
|
||||
case "max":
|
||||
return Binary(name, args, MathF.Max);
|
||||
case "pow":
|
||||
return Binary(name, args, MathF.Pow);
|
||||
case "clamp":
|
||||
return Ternary(name, args, (x, lo, hi) => Math.Clamp(x, lo, hi));
|
||||
case "lerp":
|
||||
return Ternary(name, args, (a, b, t) => a + (b - a) * t);
|
||||
case "step":
|
||||
return Binary(name, args, (edge, x) => x < edge ? 0f : 1f);
|
||||
default:
|
||||
throw new FormulaException($"Unknown function '{name}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Node Unary(string name, List<Node> args, Func<float, float> op)
|
||||
{
|
||||
Require(name, args, 1);
|
||||
var a = args[0];
|
||||
return ctx => op(a(ctx));
|
||||
}
|
||||
|
||||
private static Node Binary(string name, List<Node> args, Func<float, float, float> op)
|
||||
{
|
||||
Require(name, args, 2);
|
||||
var a = args[0];
|
||||
var b = args[1];
|
||||
return ctx => op(a(ctx), b(ctx));
|
||||
}
|
||||
|
||||
private static Node Ternary(string name, List<Node> args, Func<float, float, float, float> op)
|
||||
{
|
||||
Require(name, args, 3);
|
||||
var a = args[0];
|
||||
var b = args[1];
|
||||
var c = args[2];
|
||||
return ctx => op(a(ctx), b(ctx), c(ctx));
|
||||
}
|
||||
|
||||
private static void Require(string name, List<Node> args, int count)
|
||||
{
|
||||
if (args.Count != count)
|
||||
{
|
||||
throw new FormulaException(
|
||||
$"Function '{name}' expects {count} argument(s) but got {args.Count}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user