Calculation Engine

Expression engine architecture, function registry, evaluation lifecycle, and cross-platform parity

Overview

The calculation engine powers all expression evaluation across the MQUAD platform. It handles five expression types using a unified parser and evaluator:

Expression TypePurposeReactivity
calculationComputed field valuesReactive — re-evaluated on every change
constraintValidation rulesReactive — checked on every change
relevantSkip logic / visibilityReactive — re-evaluated on every change
choice_filterDynamic choice filteringReactive — re-evaluated on every change
defaultInitial field valuesInit-only — evaluated once at form start

Three Implementations, One Spec

All three runtimes implement the same grammar:

RuntimeFileLanguage
Serverform-service/.../xpath_evaluator.pyPython
Webfrontend/.../calculation-engine.tsTypeScript
Mobilemobile/.../expression/ (tokenizer, parser, evaluator, functions)Dart

A 114-test compatibility harness enforces identical behavior across all platforms. Every function added to one engine must be added to all three.

Architecture

Pipeline

Raw Expression String
        │
        ▼
   ┌─────────┐
   │  LEXER   │  tokenize() → Token[]
   │         │  Single-pass, left-to-right scanner
   └────┬────┘
        │
        ▼
   ┌─────────┐
   │  PARSER  │  parse() → AST
   │         │  Recursive descent, operator precedence
   └────┬────┘
        │
        ▼
   ┌──────────┐
   │VALIDATOR  │  validate() → ValidationResult
   │          │  Variable existence, function names, parens balance
   └────┬─────┘
        │
        ▼
   ┌──────────┐
   │EVALUATOR  │  evaluate() → Value
   │          │  Walk AST, resolve variables, call functions
   └──────────┘

Dependency Graph

The platform uses a directed acyclic graph (DAG) to manage expression evaluation order:

  1. Node: Each expression keyed by {type}:{question_name} (e.g., calculation:total_score)
  2. Edge: A → B means "A depends on B" (B must be evaluated before A)
  3. Topological Sort: Kahn's algorithm determines evaluation order
  4. Partial Recalculation: When a field changes, getAffectedByChange() returns only the transitive dependents that need re-evaluation
  5. Cycle Detection: DFS with recursion stack; self-references are ignored

Expression Syntax

Literals

TypeExamples
Number42, -3.14, 0
String'hello', "world"
Booleantrue(), false()
Nullnull

Variable References

${question_name}

References another question's answer by its name field. The dot . represents the current question's value.

Operators

CategoryOperatorsPrecedence
Logical ORor1 (lowest)
Logical ANDand2
Comparison=, ==, !=, <, >, <=, >=3
Addition+, -4
Multiplication*, div, mod5
Unary-, not, !6 (highest)

Ternary

if(condition, thenValue, elseValue)

Function Reference

Numeric Functions

FunctionSignatureDescription
int(v)any → int?Truncate to integer
number(v)any → num?Convert to number
round(v [, n])num, int? → numRound to n decimal places
abs(v)num → numAbsolute value
min(a, b, ...)num... → num?Minimum value
max(a, b, ...)num... → num?Maximum value
sum(a, b, ...)num... → num?Sum of values

String Functions

FunctionSignatureDescription
concat(a, b, ...)any... → stringConcatenate values
join(sep, a, b, ...)string, any... → stringJoin with separator
string(v)any → stringConvert to string
string-length(v)string → intLength of string
substring(s, start [, end])string, int, int? → stringExtract substring
contains(s, sub)string, string → boolCheck for substring
starts-with(s, prefix)string, string → boolCheck prefix
ends-with(s, suffix)string, string → boolCheck suffix
replace(s, search, repl)string, string, string → stringReplace all occurrences
regex(v, pattern)string, string → bool?Test regex match

Date Functions

FunctionSignatureDescription
today()→ stringCurrent date (yyyy-MM-dd)
now()→ stringCurrent datetime (ISO 8601)
date(v)string → string?Parse date string
date(y, m, d)int, int, int → string?Construct date from components
format-date(d, fmt)string, string → stringFormat date with pattern
days-between(d1, d2)string, string → int?Days between dates
today-offset(n)int → stringDate offset from today
year(d)string → int?Extract year
month(d)string → int?Extract month (1-12)
day(d)string → int?Extract day (1-31)

Selection Functions

FunctionSignatureDescription
selected(v, choice)array, string → stringCheck choice selected
count-selected(v)array → intCount selected choices
selected-at(v, i)array, int → stringChoice at index

Conditional Functions

FunctionSignatureDescription
if(cond, t, e)bool, any, any → anyConditional
coalesce(a, b, ...)any... → any?First non-empty value
not(v)any → boolLogical NOT

Utility Functions

FunctionSignatureDescription
count(v)any → intCount items
uuid()→ stringGenerate UUID v4

Evaluation Lifecycle

Form Start

1. Load schema + prefill data
2. Resolve default values (date/static eagerly, refs in order)
3. Build dependency graph from all expressions
4. Evaluate ALL calculation expressions (topological order)
5. Render UI

On Field Change

1. Update field value in answers
2. Query dependency graph for affected nodes
3. Re-evaluate only affected calculation expressions
4. Re-check constraints for affected questions
5. Re-evaluate relevance for affected questions
6. Re-compute choice filters for affected questions
7. Update UI

Compatibility

All functions and operators are tested across all three platforms with 114 canonical test cases. See ODK_COMPATIBILITY_MATRIX.md for the current status and SUPPORTED_FUNCTIONS.md for the complete function registry.

Reference Documentation

  • SUPPORTED_FUNCTIONS.md — Complete function registry with signatures and examples
  • DEPENDENCY_GRAPH_SPEC.md — Dependency graph data structures and algorithms
  • EXPRESSION_ENGINE_SPEC.md — Unified expression engine architecture
  • ODK_COMPATIBILITY_MATRIX.md — Cross-platform function support tracking
  • default-values.md — Default value expression syntax and resolution rules