Files
attune/work-summary/2026-02-28-expression-engine.md
2026-03-01 20:43:48 -06:00

5.7 KiB

Expression Engine for Workflow Templates

Date: 2026-02-28 Scope: crates/common/src/workflow/expression/, crates/executor/src/workflow/context.rs

Summary

Implemented a complete expression evaluation engine for workflow templates, replacing the previous simple dot-path variable lookup and string-based condition evaluation with a full recursive-descent parser and evaluator operating over JSON values.

Motivation

Workflows previously had no way to perform calculations, comparisons, or transformations within template expressions ({{ }}). The evaluate_condition() method could only check if a rendered string was truthy — it couldn't evaluate expressions like result().code == 200 and succeeded() or length(items) > 3. Publish directives and task inputs had no way to compute derived values.

Architecture

The engine follows a classic three-phase interpreter design, located in crates/common/src/workflow/expression/:

  1. Lexer (tokenizer.rs) — converts expression strings into tokens
  2. Parser (parser.rs) — recursive-descent parser producing an AST
  3. Evaluator (evaluator.rs) — walks the AST against an EvalContext trait to produce JsonValue results

The EvalContext trait decouples the expression engine from the WorkflowContext, allowing it to be used in other contexts (e.g., rule condition evaluation, template resolver) in the future.

Operator Precedence (lowest to highest)

  1. or
  2. and
  3. not (unary)
  4. ==, !=, <, >, <=, >=, in
  5. +, -
  6. *, /, %
  7. Unary -
  8. Postfix: .field, [index], (args)

Supported Operators

Arithmetic

  • + — number addition, string concatenation, array concatenation
  • -, *, /, % — standard numeric operations
  • Unary - — negation
  • Integer division returns float when not evenly divisible (e.g., 10 / 42.5, 10 / 52)

Comparison

  • ==, != — deep equality (recursive for objects/arrays, cross-type for int/float)
  • >, <, >=, <= — ordering for numbers, strings, and lists
  • No implicit type coercion: "3" == 3false

Boolean

  • and, or — short-circuit evaluation
  • not — logical negation

Membership & Access

  • . — object property access
  • [n] — array index (supports negative indexing), object bracket access, string character access
  • in — membership test (item in array, key in object, substring in string)

Built-in Functions

Type Conversion

string(v), number(v), int(v), bool(v)

Introspection

type_of(v), length(v), keys(obj), values(obj)

Math

abs(n), floor(n), ceil(n), round(n), min(a,b), max(a,b), sum(arr)

String

lower(s), upper(s), trim(s), split(s, sep), join(arr, sep), replace(s, old, new), starts_with(s, prefix), ends_with(s, suffix), match(pattern, s) (regex)

Collection

contains(haystack, needle), reversed(v) (arrays and strings), sort(arr), unique(arr), flat(arr), zip(a, b), range(n) / range(start, end), slice(v, start, end), index_of(haystack, needle), count(haystack, needle), merge(obj_a, obj_b), chunks(arr, size)

Workflow-specific (via EvalContext)

result(), succeeded(), failed(), timed_out()

Design Decisions

  • No implicit type coercion: Arithmetic between strings and numbers is an error. Only int/float cross-comparison is allowed (e.g., 3 == 3.0true).
  • Python-like truthiness: null, false, 0, "", [], {} are falsy; everything else is truthy.
  • Integer preservation: Operations on two integers produce integers; operations involving any float produce floats. Integer division that isn't evenly divisible promotes to float.
  • Array literals: Supported in expressions: [1, 2, 3] (with optional trailing comma).
  • Negative array indexing: arr[-1] returns the last element.

Integration with WorkflowContext

  • WorkflowContext implements the EvalContext trait, bridging variable resolution (parameters, item, index, system, task, variables, and direct variable names) and workflow functions (result(), succeeded(), failed(), timed_out())
  • evaluate_expression() now delegates entirely to the expression engine
  • evaluate_condition() tries the expression engine first (for bare expressions like x > 5), with fallback to template rendering for backward compatibility with {{ }} wrapper syntax
  • All existing tests pass without modification — backward compatibility is fully preserved

Test Coverage

  • 63 tests in crates/common/src/workflow/expression/ covering tokenizer, parser, and evaluator
  • 28 tests in crates/executor/src/workflow/context.rs (17 existing + 11 new) covering integration with WorkflowContext
  • New tests cover: condition comparisons, boolean operators, in operator, function calls in conditions, length() in conditions, arithmetic in templates, string concatenation, nested function calls, bracket access, and type conversion

Files Changed

  • Created: crates/common/src/workflow/expression/mod.rs — module entry point and integration tests
  • Created: crates/common/src/workflow/expression/ast.rs — AST node types
  • Created: crates/common/src/workflow/expression/tokenizer.rs — lexer
  • Created: crates/common/src/workflow/expression/parser.rs — recursive-descent parser
  • Created: crates/common/src/workflow/expression/evaluator.rs — AST evaluator with all built-in functions
  • Modified: crates/common/src/workflow/mod.rs — added expression module
  • Modified: crates/executor/src/workflow/context.rs — integrated expression engine, replaced old evaluate_expression/evaluate_condition, implemented EvalContext trait, removed unused get_nested_value, added new tests