ZMBT Expressions¶
This document is in progress
ZMBT utilizes an embedded functional programming language for the test data manipulation and matching, referred in the documentation simply as expressions.
The language resides in the zmbt::expr
namespace and consists of keywords that can be parametrized and combined into a single expression, resulting in a pure function of type JSON -> JSON
, which is evaluated by test model runners. The language belongs to a family of tacit programming languages. As it operates on JSON, certain elements may resemble the
jq language, however, expressions focus more on a simpler syntax
and certain test-specific features like typed operator handling.
Syntax¶
Expression keywords are grouped by their resulting arity, counting both design-time and evaluation-time parameters.
General syntax: Keyword
or Keyword(<Expression list>...)
, where the second form is a design-time parametrization, but not yet an evaluation.
Both forms yield a Expression
object with an eval
method, used by the framework at runtime.
The >>
operator is syntactic sugar for eval
, and is used in examples below for brevity.
Form | Resulting Expression Type | Example |
---|---|---|
Const | \(E^C \mapsto (x \mapsto C)\) | Pi >> null = 3.1415... |
Unary | \(E^f \mapsto (x \mapsto f(x))\) | Sin >> \(\frac{\pi}{2}\) = 1 |
Binary₁ | \(E^* \mapsto ([x, y] \mapsto x * y )\) | Add >> [2,2] = 4 |
Binary₂ | \(E^*(y) \mapsto (x \mapsto x * y )\) | Eq(42) >> 13 = ⊥ |
Binary₃ | \(E^* \mapsto (x \mapsto x * default)\) | Max >> [-1,1] = 1 |
Ternary | \(E^f(a, b) \mapsto (x \mapsto f(a, b)(x))\) | Recur(Pow(2), 4) >> 4 = 65536 |
Variadic | \(E^f(a,b,c,...) \mapsto (x \mapsto f(a,b,c,...)(x))\) | All(Gt(5), Le(6)) >> 6 = ⊤ |
Literal₁ | Evaluated as is where a value is expected | Map(Eq(0)) ≢ Map(0) |
Literal₂ | Evaluated as Eq(value) where a predicate is expected |
Filter(42) ≡ Filter(Eq(42)) |
The Const keywords creates constant functions. They are syntactically equivalent to Unary, with the difference that constants will ignore the input value.
Binary keywords have the most flexible syntax. The canonic Binary₁ form with no parameters like Add
expects
a pair at eval-time input, but Binary₂ form like Add(42)
essentially creates a curried unary
functor with bound right-hand side operand. To curry a left-hand side operand , the Flip
keyword may be helpful.
This is especially useful for non-commutative operators, e.g.:
For the Binary₁ the composition with Reverse
can be utilized instead of Flip
to get the proper commutation, as Flip
swaps the design-time and eval-time arguments, which differs it from Huskell flip.
The predicates in Binary₂ form are very similar to GoogleTest matchers, e.g. Eq(42)
or Lt(0.5)
.
It may be also helpful for understanding to look this form from OOP perspective, considering it as
a class method on eval-time argument object. E.g.,
The Binary₃ form replaces the Binary₁ behavior for a small group of expressions that have the
default rhs value, e.g. Max(Id)
is equivalent to just Max
, where the identity expression Id
is a default parameter (a key function in this case).
The Ternary and Variadic keywords, with a few exceptions,+ follows the same evaluation rule as Binary1 vs Binary₂ for cases with no design-time parameters, e.g. variadic
are both valid and produce the same result.High-order keywords and transforms¶
Several keywords produce high-order expressions that are useful for creating a complex matchers or generators.
The most powerful in this group are Apply
, Compose
, and Pack
, which also can be expressed with
overloaded infix operators for brevity (listed in precedence order from highest to lowest):
Operator | keyword | Description |
---|---|---|
<< | Apply | bind run-time x at design time |
| | Compose | compose expressions |
& | Pack | evaluate and pack results into an array |
Do not confuse the Apply left shift operator <<
with right shift >>
which stands for inline evaluation.
Other useful keywords are:
Filter
,Map
,Reduce
- similar to Python functools, e.g.:At
,Transp
,Slide
- powerfool data transformers, e.g.:Slide(3)|Map(Avg)
: moving average with step width = 3At("key")
,At(0)
- simple element gettersAt("/foo/bar")
- JSON pointer queryAt("::2")
- array slice query
Saturate
,All
,Any
,Count
- matcher building elements
For the complete information see Expression DSL reference.
Debug¶
Complex expressions evaluation
Expression::EvalContext ctx{};
ctx.log = Expression::EvalLog::make();
auto const f = Reduce(Add) & Size | Div;
auto const x = L{1,2,3,42.5};
f.eval(x, ctx);
std::cerr << ctx.log << '\n';
Produced output is printed bottom-up in order of evaluation:
┌── ":add"([1,2]) = 3
├── ":add"([3,3]) = 6
├── ":add"([6,4.25E1]) = 4.85E1
┌── {":reduce":":add"}([1,2,3,4.25E1]) = 4.85E1
├── ":size"([1,2,3,4.25E1]) = 4
┌── {":pack":[{":reduce":":add"},":size"]}([1,2,3,4.25E1]) = [4.85E1,4]
├── ":div"([4.85E1,4]) = 1.2125E1
□ {":compose":[":div",{":pack":[{":reduce":":add"},":size"]}]}([1,2,3,4.25E1]) = 1.2125E1
f(x) = result
, and connected with line-drawing to show the expression terms hierarchy.
In model tests, the evaluation stack is logged on failing tests.
For the bulky log messages the elements are trimmed with ...
while trying to keep evaluation result visible: