Skip to content

Natural language expression engineA calculation engine you embed.

Give it a line of text and it hands back a value, with units, currencies, dates and plain English already worked out. Nothing to render and no DOM to provide, so it runs in Node, a browser or a worker.
npm install solve-engine
  • MIT licensed
Live

This is the engine, not a video of it.

Every block on this page is a real instance running in your browser. Click into one and change a number. The answers follow, and so does everything downstream of them.

# Studio job, quote
rate = 65 USD
hours = 18.5
labour = rate * hours
materials = 240 USD
travel = 12 miles in km
subtotal = labour + materials
vat = 20% of subtotal
total = subtotal + vat
deposit = 30% of total
balance = total - deposit
The API

One call. Every line.

Hand the engine a document and it returns a value per line, along with where each one came from. That is the whole integration.

Results are plain values, not strings. Ask one what type it is, what unit it carries, or what number sits underneath, and format it however your product needs to.

The engine keeps a dependency graph across lines, so an edit re-evaluates the lines that actually depended on it and nothing else. That is what makes it safe to call on every keystroke.

import { ExpressionEngine } from "solve-engine";
import { formatValue } from "solve-engine/format";
const engine = new ExpressionEngine("en");
const { lines } = engine.parseDocument(document, {
inputType: "markdown",
});
for (const line of lines) {
if (!line.result) continue;
render(line.lineNumber, formatValue(line.result));
}
What it does

Your users write sentences.
You get values.

1024 * 8
(2 + 3) * 4
2^10
17 mod 5
2.5k
15% of 2400
increase 100 by 10%
100 to 150
5% of what is 6
01

Where you run the numbers

No function to call and no syntax to learn first. A line that looks like a sum is a sum, and everything else is left alone.

  • Percentages in the several senses people mean them by
  • Magnitude suffixes, so 2.5k reads as 2,500
  • Powers, remainders and parentheses, typed as written
100 cm + 2 m
5 miles in km
72F to C
2 cups to ml
250 kg to pounds
1 GB in MB
30 fps * 3 minutes
2 hours + 45 minutes
02

Units that survive the arithmetic

Not string matching on a suffix. Units take part in the calculation, and the result keeps the right one.

  • Length, mass, volume, data, temperature and time
  • Mixed units in one expression, converted on the way
  • Incompatible units refused rather than quietly added
# Weekly shop
veg = 18.40
meat = 24.10
cupboard = 31.75
weekly = veg + meat + cupboard
monthly = weekly * 4.33
yearly = weekly * 52
split(n) = weekly / n
split(4)
03

A document is one calculation

Name a value on one line and use it on the next. Change a number near the top and everything below it moves.

  • Names defined by assignment, no declaration step
  • Edits re-evaluate only the lines that depended on them
  • Functions are defined the same way values are
# Trip planning
depart = 12/09/2026
depart + 11 days
depart + 5 workdays
days until 25/12/2026
days since 01/01/2026
next friday
14:00 to 17:30
04

Dates, times and working days

Calendar questions phrased the way you would ask them out loud, and answered in the unit you asked in.

  • Relative dates such as next friday or today plus three weeks
  • Differences in days, weeks or working days
  • Clock times and durations in the same arithmetic
average of 12, 19, 7, 24
[1, 2; 3, 4] * [5, 6; 7, 8]
factor(x^2-4)
solve(x^2-3x+2=0, x)
der(x^3, x)
05

Statistics, matrices, algebra

Same syntax, same line. Live figures such as currencies and stock prices resolve asynchronously, and a value that is still loading stays a distinct kind of value rather than becoming a zero.

  • Aggregates over a list, written the way you would say it
  • Vectors and matrices with real matrix multiplication
  • Factoring, solving and calculus, on exact rational coefficients
Highlighting

The colours ship with the engine.

Solve classifies a line into semantic categories and leaves the rendering to you. The notepads above use exactly this, which is why the expressions in them are coloured at all.

getSemanticTokens returns a category per span. tokenClassName turns a category into a stable class name, so a package that registers a brand new token type gets a class without anyone editing an adapter.

The names are namespaced on Solve, not on any editor, and the prefix is yours to change if your app already owns one. Colours are ordinary CSS. The same categories drive completions through getCompletions, so one integration covers both.

42
number
"gbp"
string
if
keyword
+
operator
>=
comparison
&
bitwise
sqrt
function
total
variable
km
unit
vec2
vector
(
punctuation
import { LanguageService, tokenClassName } from "solve-engine/language";
const language = new LanguageService(engine);
for (const token of language.getSemanticTokens(lineText, lineNumber)) {
paint(token.from, token.to, tokenClassName(token.category));
// "number" becomes "solve-number"
}
.solve-number { color: var(--solve-hl-number); }
.solve-unit { color: var(--solve-hl-unit); }

Already own a namespace? Bind your own prefix once and use it everywhere.

import { createTokenClassName } from "solve-engine/language";
const className = createTokenClassName("cm-solve-");
className("number"); // "cm-solve-number"
Packages

Add your own syntax.

Every feature in the engine is a package, arithmetic included. A package is a plain object, and every field on it is optional, so you declare only the part of the language you are adding.

A package can contribute vocabulary to the lexer, parsing rules, functions the virtual machine can call, token rewrites, conversion targets, variable sources, async resolvers, highlighting categories and completions.

engineVersion is checked when you register, so a package built against an incompatible engine is refused with a clear message instead of failing somewhere strange later.

The hard part is not the code, it is choosing syntax that does not collide with ordinary prose. Trigger words covers that, and writing a package covers the rest.

import type { IEnginePackage } from "solve-engine";
export const gamePackage: IEnginePackage = {
name: "osrs",
engineVersion: "^1.0.0",
lexerVocabulary: osrsVocabulary,
normalizerRules: [osrsItemNormalizerRule()],
prefixParselets: [
{ tokenType: "GAME_ITEM", parselet: new GameItemParselet() },
],
pluginFunctions: [
{ index: OSRS_PLUGIN_FN_IDX, handler: resolveGameItem },
],
asyncResolvers: [new OsrsAsyncResolver()],
// A category nobody has seen before. It gets a matching
// "solve-osrs-item" class in any editor, with no adapter changes.
tokenCategories: { GAME_ITEM: "osrs-item" },
};
import { ExpressionEngine } from "solve-engine";
const engine = new ExpressionEngine("en", false, undefined, undefined, [
...BUILTIN_PACKAGES,
gamePackage,
]);
Presentation

Evaluation and display are separate.

The engine produces a value. Turning it into text is a second step you control, and one you can skip entirely if your product renders values its own way.

formatValue covers the common case, with settings for decimal places, thousands separators, currency display and date format, all defaulting from the engine’s locale.

The leading = marker suits an editor gutter. Strip it anywhere else, which is what the notepads on this page do.

import { formatValue } from "solve-engine/format";
formatValue(value);
// "= 300.00 cm"
formatValue(value, {
numberResult: { decimalPlaces: 4, useThousandsSeparator: false },
});
// "= 300.0000 cm"
value.type; // the kind of thing it is
value.toNumber(); // the number underneath
Built for editors

Fast enough to run on every keystroke.

Bytecode, not a tree walk

Expressions compile to bytecode and run on a purpose-built virtual machine, with the compiled program cached per line.

Incremental by dependency

A graph tracks which lines read which names, so an edit re-evaluates only the lines that actually depended on it.

Honest about the unknown

A value waiting on live data is its own type. Errors propagate as errors rather than quietly becoming numbers.

No host to satisfy

No DOM, no editor coupling, no framework. It runs in Node, in a browser, in a worker or on a server.

Start with embedding the engine, or editor integration if you are wiring it to a live document like the ones above.

Correctness

6,792tests

Run on every commit, on every supported version of Node, before anything reaches npm.

285
spec files, covering the lexer, the normaliser, the parser, the virtual machine and every shipped package.
332
examples in this documentation, each one executed by the suite and checked against its printed answer.
20
language packages registered by default, each one tested on its own and again in combination.

The documentation figure is the one worth dwelling on. Every example you will read on this site is input the engine actually evaluates during the test run, compared against the result printed beside it. A page cannot drift away from the engine without the build going red first.

Security

It evaluates untrusted input on purpose.

A calculator’s job is to evaluate whatever somebody typed, so hostile input is the normal case rather than the edge case. Five claims, each one something you can check rather than take on trust.

  • No eval, no Function constructor No code generation anywhere in the source. Expressions compile to a fixed instruction set and run on a machine that can only do what its opcodes do.

  • No I/O of its own No file reads, no processes, no sockets. The only requests belong to live data, when an expression asks for a rate or a forecast.

  • One runtime dependency @tanstack/query-core. The parser, the virtual machine, the unit table and the computer algebra are all in the repository.

  • 2.6 million fuzz cases, zero process deaths Seeded and shrinking, against the expression grammar and the bytecode VM. Every finding is committed as a reproducer.

  • Bounded by construction Length, nesting, instructions, stack, collection size, total allocation and call count are all capped, each with a named error.

The security page shows how to check every one of those, and hands you a live engine to type something hostile into.

Size

106 kBminified and gzipped

That is what reaches your users when you import the engine, measured by bundling it for real on every commit. It is a lot for a library, and this is not a library: it is a language and its runtime. What that buys is broken down directly below.

1
runtime dependencies, so the number above is close to what your lockfile grows by. The weight is the engine, not somebody else's code arriving behind it.
2.5 MB
downloaded from npm, across 251 files.
10.0 MB
on disk once installed, nearly all of it TypeScript declarations and the CommonJS build. None of it reaches a browser.

One thing worth being straight about: importing a single name costs within 9 bytes of importing everything, because every subpath reaches the engine core. The 16 subpath exports exist so you can import from where a thing lives and so a bundler can resolve types correctly, not because they let you take a smaller slice. If that changes, this paragraph changes with it.

What's in the box

A language, not a maths helper.

A real front end

A lexer, a Pratt parser, a bytecode compiler and a virtual machine. Text goes in one end and instructions come out the other.

Quantities, not numbers

Units with dimensional analysis, currencies, dates, times and timezones. The kind that refuse to be added when they should not be.

Matrices and models

Matrix arithmetic with determinants and inverses, statistics over ranges, and the finance functions people actually reach for.

A computer-algebra system

Factors, differentiates, integrates and solves, over exact rational and complex arithmetic rather than floating point.

That is 20 built-in packages, all of it inside the figure above and none of it fetched at runtime. Nothing here is an optional extra you install afterwards, and nothing here calls out to a service to do the work.

Oyren

Many apps. One workspace.

Open a document and the tool it needs is already open with it. No second app to buy, no import step, no second copy quietly going out of date. Windows, macOS, Linux and the browser, all the same workspace.

Solve is the calculation engine inside it.