Evaluation
Evaluating an expression is the process of determining the value of the expression. This involves looking up the definitions of symbols and functions, evaluating the arguments of functions, and applying the function to the arguments.
Evaluation Methods
To evaluate an expression, use the expr.evaluate()
method.
Numeric Approximation
By default, expr.evaluate()
preserves exact values in the result.
To force numeric evaluation use the numericApproximation
option.
The expr.N()
method is a shorthand for expr.evaluate({numericApproximation: true})
.
Compilation
An expression can be evaluated by compiling it to JavaScript using the expr.compile()
method.
Asynchronous Evaluation
Some computations can be time-consuming. For example, computing a very large factorial. To prevent the browser from freezing, the Compute Engine can perform some operations asynchronously.
To perform an asynchronous evaluation, use the expr.evaluateAsync()
method.
The expr.evaluateAsync()
method returns a Promise
that resolves to the result
of the evaluation. It accepts the same numericApproximation
options as expr.evaluate()
.
It is also possible to interrupt an evaluation, for example by providing the user with a pause/cancel button.
To make an evaluation interruptible, use an AbortController
object and a signal
.
For example, to interrupt an evaluation after 500ms:
To set a time limit for an operation, use the ce.timeLimit
option, which
is a number of milliseconds.
The time limit applies to both the synchronous or asynchronous evaluation.
The default time limit is 2,000ms (2 seconds).
When an operation is canceled either because of a timeout or an abort, a
CancellationError
is thrown.
Scopes
The Compute Engine supports lexical scoping.
A scope includes a symbol table, which is a collection of definitions for symbols and functions.
Scopes are arranged in a stack, with the current (top-most) scope available with
ce.context
.
To locate the definition of an identifier, the symbol table associated with the current (top-most) scope is searched first. If no matching definition is found, the parent scope is searched, and so on until a definition is found.
To add a new scope to the context use ce.pushScope()
.
ce.pushScope();
ce.assign('x', 500); // "x" is defined in the new scope
To exit a scope use ce.popScope()
.
This will invalidate any definition associated with the scope, and restore the symbol table from previous scopes that may have been shadowed by the current scope.
Binding
Name Binding is the process of associating an identifier (the name of a function or symbol) with a definition.
Name Binding should not be confused with value binding with is the process of associating a value to a symbol.
For symbols, the definition records contain information such as the type of the symbol and its value. For functions, the definition record include the signature of the function (the type of the argument it expects), and how to simplify or evaluate function expressions that have this function as their head.
Name binding is done during canonicalization. If name binding failed, the
isValid
property of the expession is false
.
To get a list of the errors in an expression use the expr.errors
property.
Evaluation Loop
This is an advanced topic. You don't need to know the details of how the evaluation loop works, unless you're interested in extending the standard library and providing your own function definitions.
Each identifier (name of symbol or function) is bound to a definition within
a scope during canonicalization. This usually happens when calling
ce.box()
or ce.parse()
, but could also happen during expr.evaluate()
if
expr
was not canonical.
When a function is evaluated, the following steps are followed:
-
If the expression is not canonical, it is put in canonical form
-
Each argument of the function are evaluated, left to right.
-
An argument can be held, in which case it is not evaluated. Held arguments can be useful when you need to pass a symbolic expression to a function. If it wasn't held, the result of evaluating the expression would be used, not the symbolic expression.
A function definition can indicate that one or more of its arguments should be held.
Alternatively, using the
Hold
function will prevent its argument from being evaluated. Conversely, theReleaseHold
function will force an evaluation. -
If an argument is a
["Sequence"]
expression, treat each argument of the sequence expression as if it was an argument of the function. If the sequence is empty, ignore the argument.
-
-
If the function is associative, flatten its arguments as necessary. \[ f(f(a, b), c) \to f(a, b, c) \]
-
Apply the function to the arguments
-
Return the canonical form of the result