Files
mrgameeng/src/MrGameEng.Core/Formulas/FormulaFunctions.cs
T
Leonid PershinandClaude Opus 4.8 10898b08a0
CI / build-test (push) Successful in 1m19s
Add formula engine: data-driven expression evaluator (Core)
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>
2026-06-12 23:26:06 +03:00

91 lines
3.0 KiB
C#

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}."
);
}
}
}