Search guides and the APICtrl K
  1. API reference
  2. Data, text and crypto

expression.h

Arithmetic parsed from text, with named symbols, evaluated at runtime.

#include <sonore/expression.h>
  • 2types
On this page

Design notes

Why it is here after all

The earlier answer was that a generated plugin's arithmetic is code, which the compiler checks. True of the plugin's own DSP, and still the right place for it. It is not true of what a USER types: a value box that takes "440 * 2" or "-6 + 1.5", a modulation amount entered as a formula, a tuning written as ratios, a preset format that stores a curve as an expression. Those arrive at runtime as text, and the choice is between parsing them properly once, here, or badly in every plugin that wants it.

The language

expression := term (('+' | '-') term)*
term       := unary (('*' | '/') unary)*
unary      := ('+' | '-')* primary
primary    := number | '(' expression ')'
            | symbol | function '(' [expression (',' expression)*] ')'

Numbers are decimal with an optional fraction and exponent, read with '.' as the point whatever locale the host set. Symbols and functions are identifiers, dotted ones included ("lfo.rate"), resolved through a Scope at evaluation -- so one parsed expression can be evaluated against different values without being parsed again. A symbol's value is itself an Expression, so symbols may be defined in terms of each other.

Arithmetic is IEEE double, left to right in the order written, as C++ does it: 1/0 is infinity and sqrt(-1) is NaN, not errors. Errors are for what cannot be evaluated at all -- an unknown symbol or function, the wrong number of arguments -- and are reported as messages, never thrown.

Safe against what is typed

Parsing has limits on length, parenthesis depth and term count. Evaluation has a step budget and detects a symbol defined in terms of itself. And nothing that walks a tree recurses -- evaluating, printing, simplifying and destroying all use an explicit stack -- so even an expression built in code a million operators deep cannot exhaust a thread's stack. Only the parser recurses, and only as deep as the depth limit lets it.

Not for the audio thread: evaluation allocates its working stacks. Parse and evaluate on the message thread and hand the number across.

Types 2

A parsed arithmetic expression. Immutable and cheap to copy: copies share the parsed tree, so one expression may be evaluated from several threads at once.

Built in code, an expression can share one subexpression many times over (e = e + e, repeated). evaluate() is protected from that by its step budget; toString(), simplified() and the symbol queries walk every path and are not -- don't print what you built that way. A parsed expression is always a plain tree.

class Expression::VariableScope : public Expression::Scope

Symbols with values: numbers or other expressions.

Expression::VariableScope scope;
scope.set("bpm", 120.0);
scope.set("beat", Expression::parse("60 / bpm"));
Expression::parse("beat * 4").evaluate(scope);   // 2.0
↑↓ to move↵ to openEvery public name in the headers, and every guide