Custom evaluator
What an evaluator does
Every expression in a document runs on the evaluator you pass to the renderer:
import { Evaluator } from "@uicast/expr";
const evaluator = new Evaluator({ functions: tools });
<RendererProvider implementations={impls} evaluator={evaluator}>
<EntriesRenderer entries={entries} />
</RendererProvider>;For each expression it:
- checks the source against the language, once per source;
- reports the
scopespaths it reads and the host functions it calls, so uicast knows when to re-evaluate it and what to await; - runs it with
scopes,evt,currentValue, the host functions and the language’s globals; - checks the result: only plain data leaves.
Evaluator runs step 3 in an interpreter that checks every read and call
under a budget, so the page needs no unsafe-eval. A
subclass can hand step 3 to the JavaScript engine instead.
A faster evaluator
import { Evaluator } from "@uicast/expr";
export class FunctionEvaluator extends Evaluator {
protected override toFunction(names: readonly string[], body: string) {
return new Function(...names, body);
}
}
const evaluator = new FunctionEvaluator({ functions: tools });uicast does not ship this class: copy it. toFunction runs once per
expression and gets the arguments new Function takes. Steps 1, 2 and 4 stay;
the engine runs step 3.
Same expressions, on a fast computer:
| expression | Evaluator | FunctionEvaluator | |
|---|---|---|---|
scopes.root.count > 0 | a hidden condition | ~45 ns | ~9 ns |
scopes.root.user.name | any scope read | ~75 ns | ~17 ns |
| template, two interpolations | a text prop | ~160 ns | ~37 ns |
| object literal from scope | every props.expr | ~180 ns | ~53 ns |
filter, map, reduce over 1000 rows | a derived list | 20–39 µs | 0.7–2 µs |
| a whole 1000-row table, 5 expressions per row | the realistic worst case | ~0.65 ms | ~0.23 ms |
What it costs:
- No budget. Nothing meters the engine; the
budgetoption does nothing."x".repeat(1e9)runs. - No check at run time. A name built at run time reaches whatever it names.
"abc"["sub" + "str"](1)returns"bc", and[({}).constructor.constructor][0]("…")()reachesFunction, which runs any code. unsafe-eval. The page’s Content-Security-Policy must allow it.
The model is the first line of defence
Step 1 still refuses what it can see: statements, assignment, regular
expressions, unknown names, methods outside the allow-list. Through a name
built at run time, an expression can do anything the page can: call fetch,
read cookies, change the DOM.
So you bet on the author. A production model that follows the prompt contract in your own pipeline is a fair bet. A stranger’s prompt is not, and neither is text the model reads on the way: a user’s message, a fetched page, a tool result someone else can write. Any of them can carry instructions.
The prompt tells the model that text inside data is data, not instructions.
That makes bad output rare. Under Evaluator, the interpreter stops it
whether the model meant it or not.
Use FunctionEvaluator only when nothing untrusted reaches the prompt: your
own pipeline, an admin tool, your tests.