Upgrading to 2.0
2.0 redesigns the public API: how you construct an engine, what an evaluation
returns, how you detect a failure, and which packages ship by default. Every
change below is mechanical to adopt, and each is shown as before / now. The
evaluation grammar itself, the expressions your users type, is unchanged.
If you only do one thing: swap new ExpressionEngine(...) for createEngine()
and drop the [0] off every evaluateExpression/evaluateLine result. That
covers the two changes almost every consumer hits.
Packages are explicit now
Section titled “Packages are explicit now”The constructor used to register every built-in package. It now registers only
the packages you give it, so a bundler can drop the built-ins you never import.
A bare engine therefore recognises nothing: 2 + 2 on it is an undefined-token
parse error, not 4.
For the “I want everything” case, createEngine() is batteries-included:
// beforeconst engine = new ExpressionEngine();// nowimport { createEngine } from "solve-engine";const engine = createEngine();For a slim bundle, pass the packages you want. Import them individually from
solve-engine/packages so the rest tree-shake away, rather than filtering
BUILTIN_PACKAGES (which pulls the whole set into your bundle):
import { ExpressionEngine } from "solve-engine";import { ARITHMETIC_PACKAGE, UOM_PACKAGE } from "solve-engine/packages";const engine = new ExpressionEngine({ packages: [ARITHMETIC_PACKAGE, UOM_PACKAGE] });To add your own package on top of the built-ins, createEngine takes
extraPackages:
const engine = createEngine({ extraPackages: [myPackage] });The constructor takes an options object
Section titled “The constructor takes an options object”The five positional parameters
(locale, diagnosticMode, config, diagnosticPipeline, packages) become
a single EngineOptions object, and every field is optional:
// beforenew ExpressionEngine("en", false, config, undefined, packages);// nownew ExpressionEngine({ locale: "en", diagnostics: false, config, packages });The fields are locale, packages, config and diagnostics. The fourth
positional slot, an internal diagnostic-pipeline injection point no consumer
set, is gone. fromJSON (via EngineRestoreOptions) and the off-thread worker
runtime take the same shape.
config is merged per section, so drop the spread
Section titled “config is merged per section, so drop the spread”config now takes an EngineConfigOverride, a per-section deep partial merged
over the defaults. Name only the field you are changing; every other field in
that section keeps its default. The old shallow Partial<EngineConfig> needed
you to spread DEFAULT_CONFIG into any section you touched:
// beforenew ExpressionEngine("en", false, { vm: { ...DEFAULT_CONFIG.vm, maxCollectionSize: 1_000 },});// nownew ExpressionEngine({ config: { vm: { maxCollectionSize: 1_000 } } });evaluateLine and evaluateExpression return a Value
Section titled “evaluateLine and evaluateExpression return a Value”Both returned a single-element Value[], an array kept only for API stability.
They now return the Value itself:
// beforeconst [value] = engine.evaluateExpression("2 + 2 * 10");// nowconst value = engine.evaluateExpression("2 + 2 * 10");value.toNumber(); // 22Drop the destructuring or the [0] index wherever you read a result. The
evaluateLineDetailed method and the LineEvaluation and EvalResults types
are removed; evaluateLine/evaluateExpression are the surface.
The off-thread worker client mirrors this: its evaluateExpression now resolves
to a single SerializedValue rather than a SerializedValue[].
Faults are detectable, and evaluateNumber returns NaN
Section titled “Faults are detectable, and evaluateNumber returns NaN”An Error or a Pending value reads as the number 0 through toNumber(),
so a caller that reached for the number without checking the type could not tell
a fault apart from a real zero. Value now carries the guards to make that
distinction, the same ones the engine uses internally:
const value = engine.evaluateExpression("5 kg to m"); // an impossible conversionvalue.isError(); // truevalue.errorCode; // "INCOMPATIBLE_UNITS"value.errorMessage; // "a mass cannot be converted to a length"isPending() marks a value still waiting on async data, and isFault() covers
either. Check one before toNumber().
evaluateNumber applies the same guard, and this is a behaviour change: a
faulted expression now returns NaN where it used to return a silent 0.
| expression | evaluateNumber, before | now |
|---|---|---|
5 kg to m | 0 | NaN |
Removed surface
Section titled “Removed surface”Three groups of exports that registered into state nothing evaluated against, or duplicated a canonical one, are gone.
Package-provided variables. IEnginePackage.variableSources (and
IVariableSource, VariableResolver, IPackageRegistry.registerVariableSource,
and the solve-engine/variables subpath) are removed. A source’s variables were
registered into a resolver no evaluation path ever queried, so they were never
found during evaluation. A package that needs to expose a value contributes a
pluginFunctions entry instead.
The global registration singleton. The PackageRegistry class, the
packageRegistry singleton and the IPackageRegistry interface are removed.
They wrote into process-wide state no engine reads. Register on an engine:
// beforeimport { packageRegistry } from "solve-engine";packageRegistry.registerPackage(myPackage);// nowengine.registerPackage(myPackage);// or, at construction:const engine = createEngine({ extraPackages: [myPackage] });IEnginePackage drops variableSources, and its parselet and plugin-function
fields change shape (covered under Package descriptors are keyed, below).
symbolToCurrency. This backward-compatibility re-export of the currency
symbol alias table is removed; the table lives in uom/CurrencyAliases.ts.
Package descriptors are keyed
Section titled “Package descriptors are keyed”If you author a package, two descriptor fields change from lists to records, and plugin functions stop carrying a hand-allocated index.
prefixParselets and infixParselets are keyed by token type:
// beforeprefixParselets: [{ tokenType: "MY_FUNC", parselet: new MyParselet() }],// nowprefixParselets: { MY_FUNC: new MyParselet() },pluginFunctions is keyed by a package-local name. The engine assigns the
CALL_PLUGIN index at registration, and a parselet emits the call by that name
through the new builder.emitPluginCall(name, argCount):
// beforeconst MY_FN_IDX = allocatePluginFunctionIndex();pluginFunctions: [{ index: MY_FN_IDX, handler: myHandler }],// in the parselet:builder.emitOpcode(OpCode.CALL_PLUGIN);builder.emitIndex(MY_FN_IDX);builder.emitIndex(argCount);
// nowpluginFunctions: { myFn: myHandler },// in the parselet:builder.emitPluginCall("myFn", argCount);The name you emit must be one your descriptor’s pluginFunctions declares, or
registration is an error rather than a silent mis-dispatch. Two packages naming a
function the same is a checkPackageCompatibility warning, the later
registration winning, exactly as the other cross-package collisions already are.
The boundary: an async resolver that scans compiled bytecode still works in numeric indices, because that is what bytecode is. Recover your function’s index by the qualified name the engine files it under rather than owning a constant:
import { pluginFunctionIndexFor } from "solve-engine/vm";const idx = pluginFunctionIndexFor(`${packageName}:myFn`);The examples/osrs Grand Exchange resolver is the worked example.
Snapshots carry their packages
Section titled “Snapshots carry their packages”A snapshot’s compiled bytecode only lines up against the packages present when
it was written, and fromJSON registers no packages by default, exactly like
the constructor. Restore with the same set the snapshot was taken with:
// a snapshot from a full engineconst restored = ExpressionEngine.fromJSON(state, { packages: BUILTIN_PACKAGES });fromJSON also accepts config, diagnostics and a locale override, all
matching the constructor’s option names (the option is diagnostics, not the
old diagnosticMode).
Checklist
Section titled “Checklist”-
new ExpressionEngine(...)→createEngine()ornew ExpressionEngine({ packages }) - Drop
[0]/ destructuring offevaluateExpressionandevaluateLineresults - Drop the
DEFAULT_CONFIGspread fromconfigoverrides - Replace
packageRegistry.registerPackage(...)withengine.registerPackage(...) - Remove any
variableSourcesfrom your packages; expose values viapluginFunctions - Convert
prefixParselets/infixParseletsto token-keyed records, andpluginFunctionsto a name-keyed record; emit plugin calls withbuilder.emitPluginCall(name, argCount) - Pass
packagestofromJSONwhen restoring a snapshot - Check faults with
isError()/isFault(), and expectNaN(not0) fromevaluateNumberon a failure