Skip to Content
ExpressionsCustom evaluator

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:

  1. checks the source against the language, once per source;
  2. reports the scopes paths it reads and the host functions it calls, so uicast knows when to re-evaluate it and what to await;
  3. runs it with scopes, evt, currentValue, the host functions and the language’s globals;
  4. 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:

expressionin a documentEvaluatorFunctionEvaluator
scopes.root.count > 0a hidden condition~45 ns~9 ns
scopes.root.user.nameany scope read~75 ns~17 ns
template, two interpolationsa text prop~160 ns~37 ns
object literal from scopeevery props.expr~180 ns~53 ns
filter, map, reduce over 1000 rowsa derived list20–39 µs0.7–2 µs
a whole 1000-row table, 5 expressions per rowthe realistic worst case~0.65 ms~0.23 ms

What it costs:

  • No budget. Nothing meters the engine; the budget option 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]("…")() reaches Function, 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.

Last updated on