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.
The expr.value
property does not evaluate the expression. If the expression
is a literal, it returns the literal value. If the expression is a symbol, it
looks up the symbol in the current scope and returns its value.
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.
The method returns a JavaScript function that can be called to evaluate the expression.
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.
try {
const fact = ce.parse('(70!)!');
const factResult = await fact.evaluateAsync();
factResult.print();
} catch (e) {
console.error(e);
}
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:
const abort = new AbortController();
const signal = abort.signal;
setTimeout(() => abort.abort(), 500);
try {
const fact = ce.parse('(70!)!');
const factResult = await fact.evaluateAsync({ signal });
factResult.print();
} catch (e) {
console.error(e);
}
:::html
<div class="stack">
<div class="row">
<button id="evaluate-button">Evaluate</button>
<button id="cancel-button" disabled>Cancel</button>
</div>
<div id="output"></div>
</div>
:::js
const abort = new AbortController();
document.getElementById('cancel-button').addEventListener('click',
() => abort.abort()
);
document.getElementById('evaluate-button').addEventListener('click', async () => {
try {
document.getElementById('evaluate-button').disabled = true;
document.getElementById('cancel-button').disabled = false;
const fact = ce.parse('(70!)!');
const factResult = await fact.evaluateAsync({ signal: abort.signal });
document.getElementById('output').textContent = factResult.toString();
document.getElementById('evaluate-button').disabled = false;
document.getElementById('cancel-button').disabled = true;
} catch (e) {
document.getElementById('evaluate-button').disabled = false;
document.getElementById('cancel-button').disabled = true;
console.error(e);
}
});
To set a time limit for an operation, use the ce.timeLimit
option, which
is a number of milliseconds.
ce.timeLimit = 1000;
try {
const fact = ce.parse('(70!)!');
fact.evaluate().print();
} catch (e) {
console.error(e);
}
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.
Lexical Scopes and Evaluation Contexts
The Compute Engine supports lexical scoping.
A lexical scope is a region of the code where a symbol is defined. Each scope has its own bindings table, which is a mapping of symbols to their definitions. The definition includes the type of the symbol, whether it is a constant and other properties.
An evaluation context is a snapshot of the current state of the Compute Engine. It includes the values of all symbols currently in scope and a chain of lexical scopes.
Evaluation contexts are arranged in a stack, with the current (top-most)
evaluation context available with ce.context
.
Evaluation contexts are created automatically, for example when a new scope is created, or each time a recursive function is called.
Some functions may create their own scope. These functions have the scoped
flag set to true
. For example, the Block
function creates a new scope
when it is called: any declarations made in the block are only visible within the
block. Similarly, the Sum
function creates a new scope so that the index
variable is not visible outside the sum.
Binding
Name Binding is the process of associating a symbol with a definition.
Name Binding should not be confused with value binding which is the process of associating a value to a symbol.
To bind a symbol to a definition, the Compute Engine looks up the bindings table of the current scope. If the symbol is not found in the table, the parent scope is searched, and so on until a definition is found.
If no definition is found, the symbol is declared with a type of
unknown
, or a more specific type if the context allows it.
If the symbol is found, the definition record is used to evaluate the expression.
The definition record contains information about the symbol, such as its type, whether it is a constant, and how to evaluate it.
There are two kind of definition records:
- Value Definition Record: This record contains information about the symbol, such as its type and whether it is a constant.
- Operator Definition Record: This record contains information about the operator, such as its signature and how to evaluate it.
Name binding is done during canonicalization. If name binding failed, the
isValid
property of the expression is false
.
To get a list of the errors in an expression use the expr.errors
property.
Default Scopes
The Compute Engine has a set of default scopes that are used to look up symbols. These scopes are created automatically when the Compute Engine is initialized. The default scopes include the system scope, and the global scope.
The system scope contains the definitions of all the built-in functions and operators. The global scope is initially empty, but can be used to store user-defined functions and symbols.
The global scope is the default scope used when the Compute Engine is initialized.
Additional scopes can be created using the ce.pushScope()
method.
Creating New Scopes
To add a new scope use ce.pushScope()
.
ce.assign('x', 100); // "x" is defined in the current scope
ce.pushScope();
ce.assign('x', 500); // "x" is defined in the new scope
ce.box('x').print(); // 500
ce.popScope();
ce.box('x').print(); // 100
To exit a scope use ce.popScope()
.
This will invalidate any definitions associated with the scope, and restore the symbol table from previous scopes that may have been shadowed by the current scope.
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 symbol is bound to a definition within a lexical scope during
canonicalization. This usually happens when calling ce.box()
or ce.parse()
,
or if accessing the .canonical
property of a
non-canonical expression.
When a function is evaluated, the following steps are followed:
-
If the expression is not canonical, it cannot be evaluated and an error is thrown. The expression must be canonicalized first.
-
Each argument of the function are evaluated, left to right, unless the function has a
lazy
flag. If the function is lazy, the arguments are not evaluated.-
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.
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 result in canonical form.