Formula Engine
The formula engine evaluates small numeric expressions wherever the platform accepts a
user-entered formula — DataPointMapping expressions, archive computed columns and runtime-query
@-expressions. The expression syntax those features expose is documented in the tech guide
(Formula Expressions); this page
describes the engine from a developer's point of view: the service contract, the underlying
library, and the conventions every caller shares.
Library
The engine wraps the MathParser.org-mXparser NuGet package
(currently v6.1.1, referenced in Runtime.Engine.Formulas.csproj). mXparser is a mature
math-expression parser; the entire mXparser math collection (functions, operators, constants) is
available to expressions — nothing is disabled. Because every input and the result are double,
only the numeric part of that collection is practically useful (there are no string, unit or
object inputs). See the
mXparser math collection for the version-matched
list of everything the parser accepts.
The library is wrapped rather than used directly so that the ternary normalization, the null / NaN handling and the cast-back ladder live in exactly one place instead of being duplicated across callers.
IFormulaEngine
All evaluation goes through one service, IFormulaEngine
(Meshmakers.Octo.Runtime.Contracts.Formulas), implemented by FormulaEngine in the
Runtime.Engine.Formulas project. It is stateless and registered as a singleton via
AddFormulaEngine().
| Method | Purpose |
|---|---|
Validate(expression, arguments) | Bind test values, check syntax and evaluate; reports a NaN result as invalid. Backs the validate-expression endpoint. |
CheckSyntax(expression, argumentNames) | Syntax + reference check without evaluating — so a runtime-only division-by-zero (a / (b - b)) is not a false positive. |
EvaluateRaw(expression, arguments) | Evaluate to a raw double. NaN = could-not-evaluate; -Infinity = the null sentinel. |
Evaluate(expression, arguments, resultType) | EvaluateRaw plus the cast-back ladder; returns null for NaN / null sentinel. |
NormalizeTernary(expression) | Expose the cond ? a : b → if(cond, a, b) rewrite on its own. |
Arguments are always an IReadOnlyDictionary<string, double>; mXparser arguments are bound by name
from that dictionary. An expression that references a name not in the dictionary fails the syntax
check.
// scale and clamp a polled value, then cast to the column's stored type
var args = new Dictionary<string, double> { ["value"] = 42.0 };
object? result = _formulaEngine.Evaluate(
"min(max(value, 0), 100)", args, FormulaResultType.Double);
Conventions every caller shares
Ternary normalization
C-style cond ? a : b is rewritten to mXparser's if(cond, a, b) before evaluation
(FormulaEngine.ConvertTernaryToIf). The rewrite scans for the matching : at the same
parenthesis depth, so nested ternaries and parenthesised branches are handled:
a > 0 ? (b > 0 ? 1 : 2) : 3 → if(a > 0, (if(b > 0, 1, 2)), 3)
Validate / CheckSyntax return the rewritten string in NormalizedExpression so a UI can show
the user exactly what was evaluated.
Null sentinel and NaN
The engine reserves two double values:
double.NegativeInfinityis the null sentinel, exposed to expressions as thenullconstant. A missing / null input binds to it.double.NaNmeans the formula could not produce a value for these inputs (e.g.0 / 0).
Evaluate maps both to a CLR null — never a half-baked number. Callers persist that as SQL
NULL (computed columns) or fall back to the raw value (DataPointMapping). Validate treats a
NaN result as invalid, while CheckSyntax does not evaluate and therefore never flags a
runtime-only NaN.
Result types and the cast-back ladder
Evaluate casts the raw double back according to FormulaResultType:
FormulaResultType | Cast-back |
|---|---|
Double | the raw value |
Int | truncated to int |
Int64 | truncated to long |
Boolean | false if 0, otherwise true |
DateTime | the value is interpreted as .NET ticks |
The DateTime path pairs with the OctoMesh-specific now(addMinutes) and startOfDay(dayCount)
functions, both of which return DateTime ticks as a double. These extensions, plus the null
constant, are registered in OctoExpression (NowFunction, StartOfDayFunction).
Callers
| Caller | Project | Bound variables |
|---|---|---|
ApplyDataPointMappingsNode | octo-mesh-adapter | value (the polled source value) — uses EvaluateRaw |
CrateDbStreamDataRepository (computed columns) | octo-construction-kit-engine-mongodb | each source column by its physical column name — uses Evaluate with the column's stored ResultType |
ExpressionValidationService (validate-expression REST endpoint) | octo-communication-controller-services | value (a test value, default 42.0) — uses Validate |
When adding a new caller, depend on IFormulaEngine and call AddFormulaEngine() during service
registration; do not reference mXparser directly, so the shared conventions above keep applying.