EvalEngine - axkr/symja_android_library GitHub Wiki

The org.matheclipse.core.eval.EvalEngine class in Symja is responsible for the evaluation of expressions. The main methods for evaluating an expressions are evaluate(final IExpr expr) and evaluateNIL(final IExpr expr).

The evaluation process:

If the expression is a literal value, e.g. Integer, String, Complex, etc. then leave it unchanged and return.

Evaluate the head() of an expression and get its attributes.

Depending on the following attributes:

various arguments are evaluated. However, arguments that have the form:

also specify which arguments are evaluated or not before rewriting and function application.

At the end of this, the internal variables head, attributes (from head), and elements (of the expression) are set.

Building a New Expression

Construct a new expression using information from the head and the elements gathered in the previous step.

The substeps here are:

  • Attempt to flatten sequences in the expression unless the SequenceHold or HoldAllComplete attributes are set in the Head.
  • Change Unevaluated(expr) to expr but mark the expression as being unevaluated.
  • Flatten expressions involving nested functions if the Flat attribute was found in the Head.
  • Sort elements if the Orderless attribute was found in the Head.

Setting Up Thread Rewrite for Listable Functions

Threading is needed when the head has the Listable attribute. The method EvalAttributes.threadList() rewrites the expression: F({a,b,c,...}) as: {F(a), F(b), F(c), ...}.

Searching for a Rule in Head to Apply

Search for a rule in Head that matches the expression.

Applying Rule or Restoring Expression

If a rule was found, apply it getting back an evaluated expression. If the expression is unchanged, return the special value F.NIL.

Builtin-function application

For builtin-functions the first element (i.e. argument-0), often referred to as the "head" (or Head()) of an IAST (Symja's equivalent for function representation), is an IBuiltInSymbol.

When there are other elements (arguments 1 to n), the expression is assumed to be a Symja function call, where the function name comes from the head. If this is a built-in function, like Plus, Power, Times, ... the Symja function name is the name of a Java class derived from IFunctionEvaluator. These Symja function-like classes are described in later sections.

As described in the previous section, before invoking that Symja function, we need to check for a rewrite rule that applies to the Symja function call. If a rule is found, it will return the evaluated right-hand-side of that rule.

These rules get created on loading the module containing a subclass of IFunctionEvaluator implementing some Symja primitive function. The rules come from a file with the extension *.m which is translated to Java before the compile time.

When writing a new builtin function, there is an evaluate() method.

Here is an example for the DiagonalMatrix primitive taken from the code

  private static class DiagonalMatrix extends AbstractFunctionEvaluator {

    @Override
    public IExpr evaluate(final IAST ast, EvalEngine engine) {
        ...

        ...
        // the return value F.NIL has the special meaning "return unevaluated":
        return F.NIL;
    }

    @Override
    public int[] expectedArgSize(IAST ast) {
      // this function allows one or two arguments
      return ARGS_1_2;
    }

    @Override
    public void setUp(final ISymbol newSymbol) {
      // if necessary set attributes here:
      // newSymbol.setAttributes(ISymbol.FLAT | ISymbol.ONEIDENTITY);
    }

  }

The evaluate() method above will get called when finding an IAST whose Head value is DiagonalMatrix.

Thread Safety in EvalEngine

The EvalEngine class (or classes which delegate to EvalEngine) in Symja is not thread-safe. This means that it cannot be used concurrently by multiple threads without external synchronization. Here are the reasons why EvalEngine is not thread-safe:

Shared Mutable State

EvalEngine maintains a mutable state that is shared among all threads. This includes variables, rules, and other settings. If multiple threads modify this state concurrently, it can lead to inconsistent and unpredictable results. This is a classic problem in concurrent programming known as a race condition.

Lack of Synchronization

EvalEngine does not use any synchronization mechanisms (like synchronized blocks or Lock objects) to control access to its internal state. This means that multiple threads can access and modify the state simultaneously, leading to data races and other concurrency issues.

Thread-Local Storage

EvalEngine uses thread-local storage for some of its data, which means that each thread has its own copy of this data. While this can help to avoid some concurrency issues, it also means that changes made by one thread are not visible to others. This can lead to inconsistencies if threads need to coordinate their actions or share results.

Non-Atomic Operations

Some operations in EvalEngine are not atomic, meaning they involve multiple steps that should be completed as a single operation. If another thread interrupts these operations, it can leave the EvalEngine in an inconsistent state.

In conclusion, while EvalEngine is a powerful tool for symbolic computation, it is not designed to be used in a multithreaded environment without additional measures to ensure thread safety. If you need to use EvalEngine in a multi-threaded application, you should ensure that access to the engine is properly synchronized, or consider using separate EvalEngine instances for each thread.