Runtime evaluator

The runtime evaluator (see RuntimeEvaluator) is an engine to evaluate scalar mathematical expressions at runtime.

Summary

RuntimeEvaluator takes an expression in stack machine form at construction time and is then capable of compiling and executing it. The expression can be directly supplied in stack machine form, or it be derived from an infix expression (a mathematical expression in the usual form) by using MathExpressionParser.

The RuntimeEvaluator supports scalar operands of any type.

Usage

The steps required to use the evaluator are the following: - the evaluator is instantiated and fed with the expression it will be required to evaluate - the evaluator internal variable database is initialised by calling the ExtractVariable method - variable properties are set by using the APIs - the expression is compiled by calling the Compile() method - the expression is executed by calling the Execute() method

Instantiating the evaluator

The expression must first be fed to the RuntimeEvaluator at construction time, either typed directly:

StreamString rpnCode = "READ A\n"
                       "READ B\n"
                       "ADD\n"
                       "WRITE ret\n"
                       ;

RuntimeEvaluator expression(rpnCode);

or by converting an infix expression (e.g. via the MathExpressionParser):

StreamString infixExpr = "ret = A + B;"

MathExpressionParser parser(infixExpr);
parser.Parse();

RuntimeEvaluator expression(parser.GetStackMachineExpression());

Initialising the evaluator

First of all, the RuntimeEvaluator must know what variables are contained in the expression. This is done by calling the ExtractVariables() method. After that, variable properties can be set by using RuntimeEvaluator variable managing APIs.

ret = expression.ExtractVariables();

Setting up variables

Variable properties must then be set. Variables properties are: - type (can be one of MARTe2 supported types: Unsigned32Bit, Float64Bit etc.) - location (the memory location where the variable value will be held)

Types must be set for every input variable by using the SetInputVariableType and SetOutputVaribleType methods:

ret = expression.SetInputVariableType("theta", Float64Bit);

Output variable types can be set with SetOutputVariableType if needed. However, this is not compulsory as RuntimeEvaluator will assign them the output type of the last operation.

Memory locations can be set by using the SetInputVariableMemory and SetOutputVariableMemory methods.

float64 y;
ret = expression.SetOutputVariableMemory("y", &y);
  • In case an external location for a variable is set, the variable is considered external and any modification of its value will happen to the specified memory address.

  • In case an external location for a variable is not set, the variable is considered internal (this is the default behavior). The RuntimeEvaluator will be responsible for allocating space for all the internal variables. The memory location of internal variables will be available after compilation by calling the GetInputVariableMemory and GetOutputVariableMemory methods.

Compiling

After setting up the desired properties for variables, the expression can be compiled:

ret = expression.Compile();

From now on, variable values can be set and the expression can be executed.

Executing

Before executing, values of each variable can be updated. External variable values are updated simply by updating the memory they have been set to follow:

float64 theta = 5.0;
ret = expression.SetInputVariableMemory("theta", &theta);
ret = expression.Compile();
theta = 10.0;

Internal variables can be modified by retrieving a pointer to them:

ret = expression.SetInputVariableType("theta", Float64Bit);

float64* ptr;
ptr = (float64*)expression.GetInputVariableMemory("theta");

*ptr = 10.0;

Each time the expression is executed, all output variables are updated. If they have been set external, their values are directly available:

float64 y;
expression.SetOutputVariableMemory("y", &y);
...
expression.Compile();
...
expression.Execute();
REPORT_ERROR(ErrorManagement::Information, "Value of y is: %f", y);

if they are internal, GetOutputVariableMemory shall be used to retrieve their addresses and obtain the final value.

Variable values can also be updated in the middle of the expression:

StreamString rpnCode = "READ A\n"      // first
                       "READ B\n"
                       "ADD\n"
                       "WRITE A\n"     // output variable A is set
                       "READ A\n"      // second
                       "WRITE ret\n";

In the example above, the former READ A command reads from the input variable A, while the latter reads from the output variable A that was just set by the WRITE A command.

Variable values can be updated any time, and when invoked the Execute() method will recalculate the value of output variable based on the current value of input variables.

Further details

RuntimeEvaluator is a stack machine that reads instructions from a stack machine expression and executes them in a last in-first out fashion. All operations rely on an internal stack: operands are retrieved from the stack and results are placed in the stack.

Working principle

The RuntimeEvaluator scans the input stack machine code and the variable types. Combination of code and types during Compile() produces a list of calls to functions with specific types (the “pseudocode”) that will be executed during Execute(). Functions that will be called must be present in the functionRecords array, an array that holds all the available functions that RuntimeEvaluator can call. functionRecords is an array of RuntimeEvaluatorFunction objects.

When the RuntimeEvaluator executes an operation, it actually calls the corresponding function in functionRecords, or better calls the RuntimeEvaluatorFunction::ExecuteFunction() method of that function and passes itself to the method as the argument. The operation is then executed by RuntimeEvaluatorFunction, which is responsible for managing RuntimeEvaluator internal stack by using Pop(), Push() and Peek() methods. See RuntimeEvaluatorFunction documentation for further details.

Supported operators

This is a table of all supported operators:

Operator

Meaning

READ  var

Pushes the value of variable var from memory to the top of the stack

WRITE var

Pops the top of the stack and writes its value to variable var

CONST type val

Push a constant of value val and type type to the top of the stack

CAST type

Casts the top of the stack to type type

AND

AND operation between top two elements of the stack

OR

OR operation between top two elements of the stack

XOR

XOR operation between top two elements of the stack

GT

Greater than operation between top two elements of the stack

LT

Less than operation between top two elements of the stack

GTE

Greater or equal operation between top two elements of the stack

LTE

Less or equal operation between top two elements of the stack

EQ

Equal operation between top two elements of the stack

NEQ

Not equal operation between top two elements of the stack

ADD

Sum between top two elements of the stack

SUB

Subtraction between top two elements of the stack

MUL

Multiplication between top two elements of the stack

DIV

Division between top two elements of the stack

SIN

Sine operation on the top of the stack

COS

Cosine operation on the top of the stack

POW

Power operation between top two elements of the stack

Hint

Comparison of floating-point types may often be implementation-dependent. The EQ and NEQ operators use the SafeMath::IsEqual() instead of the standard == operator in order to be safely portable: this implementation of the comparison operation takes into account the floating-point operand granularity (machine epsilon) to achieve a safely portable equality check.

Adding new functions

New operations can be made available to the RuntimeEvaluator by adding a RuntimeEvaluatorFunction to the functionRecords as follows:

void NewOperation(RuntimeEvaluator &evaluator) {
    float32 x1,x2,x3;
    evaluator.Pop(x1);
    evaluator.Pop(x2);
    x3 = x2 + x1;
    evaluator.Push(x3);
}

TypeDescriptor types[] = {Float32Bit, Float32Bit, Float32Bit};
RuntimeEvaluatorFunction newAdd("NEWADD", 2, 1, types, NewOperation);
RegisterFunction(newAdd);

The function above pops two float32 element from the stack, sums them and then pushes the result to the stack. Upon calling the RegisterFunction function, the function itself becomes available to the RuntimeEvaluator by using the command NEWADD.

Examples

Example usage with the following expression: y = pow(sin(theta), 2) + pow(cos(theta), 2)

(for a more extendive example see below).

#include "RuntimeEvaluator.h"

bool ret;

StreamString rpnCode = "READ theta\n"
                       "SIN\n"
                       "CONST int64 2\n"
                       "POW\n"
                       "READ theta\n"
                       "COS\n"
                       "CONST int64 2\n"
                       "POW\n"
                       "ADD\n"
                       "WRITE y\n"
;

RuntimeEvaluator expression(rpnCode);

ret = expression.ExtractVariables();

float64 theta;
float64 y;

if (ret) {
    ret &= expression.SetInputVariableType("theta", Float64Bit);
    ret &= expression.SetInputVariableMemory("theta", &theta);
}

if (ret) {
    ret &= expression.SetOutputVariableType("y", Float64Bit);
    ret &= expression.SetOutputVariableMemory("y", &y);
}

if (ret) {
    ret = expression.Compile();
}

// now variable values can be set
theta = 3.14;

if (ret) {
    ret = expression.Execute();
}

// result is now available
if (y == 1.0) {
    REPORT_ERROR(ErrorManagement::Information, "OK!");
}

The following example shows how to use RuntimeEvaluator to perform runtime calculations.

Runtime evaluator example (RuntimeEvaluatorExample1)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/**
 * @file RuntimeEvaluatorExample1.cpp
 * @brief Source file for class RuntimeEvaluatorExample1
 * @date 01/07/2020
 * @author Nicolo Ferron
 *
 * @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and
 * the Development of Fusion Energy ('Fusion for Energy').
 * Licensed under the EUPL, Version 1.1 or - as soon they will be approved
 * by the European Commission - subsequent versions of the EUPL (the "Licence")
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
 *
 * @warning Unless required by applicable law or agreed to in writing, 
 * software distributed under the Licence is distributed on an "AS IS"
 * basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the Licence permissions and limitations under the Licence.

 * @details This source file contains the definition of all the methods for
 * the class LoggerServiceExample1 (public, protected, and private). Be aware that some
 * methods, such as those inline could be defined on the header file, instead.
 */

#define DLL_API

/*---------------------------------------------------------------------------*/
/*                         Standard header includes                          */
/*---------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------*/
/*                         Project header includes                           */
/*---------------------------------------------------------------------------*/
#include "AdvancedErrorManagement.h"
#include "ErrorLoggerExample.h"
#include "MathExpressionParser.h"
#include "RuntimeEvaluator.h"
#include "StreamString.h"

/*---------------------------------------------------------------------------*/
/*                           Static definitions                              */
/*---------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------*/
/*                           Method definitions                              */
/*---------------------------------------------------------------------------*/

int main(int argc, char **argv) {
    using namespace MARTe;
    // Enables the REPORT_ERROR macro
    SetErrorProcessFunction(&ErrorProcessExampleFunction);
    
    bool ok;
    StreamString expression, stackMachineExpr;
    float64 F, m1, m2, G;
    float64 sunMass   = 1.988e+30;
    float64 earthMass = 5.972e+24;
    float64 moonMass  = 7.346e+22;
    RuntimeEvaluator *evaluator = NULL_PTR(RuntimeEvaluator *);

    expression = "F = G*((m1*m2)/pow(d, 2));";
    
    REPORT_ERROR_STATIC(ErrorManagement::Information,
        "Expression:\n%s", expression.Buffer());
    
    // RuntimeEvaluator accepts expressions in stack machine instructions
    expression.Seek(0u);
    MathExpressionParser mathParser(expression);
    ok = mathParser.Parse();
    if (ok) {
        stackMachineExpr = mathParser.GetStackMachineExpression();
        
        REPORT_ERROR_STATIC(ErrorManagement::Information,
            "Expression in stack machine instruction form:\n%s", stackMachineExpr.Buffer());
    }
    
    // Instantiate the evaluator object
    if (ok) {
        evaluator = new RuntimeEvaluator(stackMachineExpr);
        ok = evaluator->ExtractVariables();
    }
    
    if (ok) {
        // Types of all variables must be specified
        ok &= evaluator->SetOutputVariableType("F", Float64Bit);
        
        ok &= evaluator->SetInputVariableType("G",  Float64Bit);
        ok &= evaluator->SetInputVariableType("m1", Float64Bit);
        ok &= evaluator->SetInputVariableType("m2", Float64Bit);
        ok &= evaluator->SetInputVariableType("d",  Float64Bit);
        
        // A variable can be set to an external location
        ok &= evaluator->SetOutputVariableMemory("F", &F);
        
        ok &= evaluator->SetInputVariableMemory("G",  &G);
        ok &= evaluator->SetInputVariableMemory("m1", &m1);
        ok &= evaluator->SetInputVariableMemory("m2", &m2);
        // Notice that address of variable d is not set and will have to be retrieved
    }
    
    // Now the expression can be compiled
    if (ok) {
        ok = evaluator->Compile();
    }
    
    // From now on, values can be updated at any time
    if (ok) {
        // External variables (G, m1, m2)
        G  = 6.674e-11;
        m1 = earthMass;
        m2 = sunMass;
        
        // Internal variable (d)
        *((float64*)evaluator->GetInputVariableMemory("d")) = 1.496e+11;
    }
    
    // The expession is executed and the value of F becomes available
    if (ok) {
        ok = evaluator->Execute();
    }
    
    if (ok) {
        REPORT_ERROR_STATIC(ErrorManagement::Information,
            "Avg. gravitational force between Sun and Earth is: %.4e N.", F);
    }
    
    // Values can be changed and the expression evaluated again
    if (ok) {
        m2 = moonMass;
        *((float64*)evaluator->GetInputVariableMemory("d")) = 3.85e+8;
    }
    
    if (ok) {
        ok = evaluator->Execute();
    }
    
    if (ok) {
        REPORT_ERROR_STATIC(ErrorManagement::Information,
            "Avg. gravitational force between Moon and Earth is: %.4e N.", F);
    }
    
    if (!ok) {
        REPORT_ERROR_STATIC(ErrorManagement::FatalError, "Evaluation failed.");
    }
    
    return 0;
}

Instructions on how to compile and execute the example can be found here.