Skip to main content

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().

MethodPurpose
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 : bif(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.NegativeInfinity is the null sentinel, exposed to expressions as the null constant. A missing / null input binds to it.
  • double.NaN means 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:

FormulaResultTypeCast-back
Doublethe raw value
Inttruncated to int
Int64truncated to long
Booleanfalse if 0, otherwise true
DateTimethe 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

CallerProjectBound variables
ApplyDataPointMappingsNodeocto-mesh-adaptervalue (the polled source value) — uses EvaluateRaw
CrateDbStreamDataRepository (computed columns)octo-construction-kit-engine-mongodbeach source column by its physical column name — uses Evaluate with the column's stored ResultType
ExpressionValidationService (validate-expression REST endpoint)octo-communication-controller-servicesvalue (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.