Equana

All articles
July 16, 2026·Max Schurigsymbolic-mathcassymwa

Symbolic math in the browser with symwa

A real computer algebra system — exact fractions, symbolic derivatives, equation solving, and simplification — running in a notebook tab next to your numeric code.

Most scientific-computing tools are built on one number type: the 64-bit float. It is fast, it is universal, and it is lossy — the moment you write 1/3 it becomes 0.3333333333333333, and the exact fraction is gone forever. A computer algebra system refuses that trade. It manipulates x, 1/3, and sin(x)² + cos(x)² as the exact mathematical objects they are, differentiates and factors and solves them by the rules of algebra, and only drops to a float when you ask it to. Equana ships one — an engine called symwa — and it runs entirely in the browser, in a tab right next to your numeric work.

symwa has had zero marketing, so this post is its introduction: what it is, how it's built, and what it lets you do that a pure-numeric notebook cannot.

What's under the hood

symwa is Equana's symbolic package, and it is two layers. Underneath sits SymEngine — a fast C++ symbolic core — compiled to WebAssembly, bundled with the GMP arbitrary-precision arithmetic library so that integers and rationals never overflow and never round. On top sits a TypeScript API that mirrors the engine's data model: an expression tree.

An expression tree is how every CAS represents math internally. x^2 + 2*x + 1 is not text and not a number — it's a tree: a top Add node with three children, one of which is a Pow node holding x and 2, another a Mul node holding 2 and x. symwa's TypeScript core has a class per node kind — Symbol, Add, Mul, Pow, and the numeric and constant leaves — so the same structure exists on both sides of the WASM boundary. Every operation, whether differentiation or simplification, is a transformation from one tree to another. Because the heavy engine is WASM and it loads on demand, the symbolic machinery costs nothing until the first symbolic call in a notebook, then it's resident for the rest of the session.

Symbols, and exact numbers

The atom of symbolic math is the symbolic variable. sym("x") creates one: an abstract x that carries no value and can be built into expressions.

x = sym("x") y = sym("y") f = x^2 + 2*x + 1 g = x*y + sin(x) println(f) println(g) x^2 + 2*x + 1 x*y + sin(x)

The same sym wraps numbers into their exact form, and this is where the difference from floats becomes concrete:

half = sym(1) / sym(2) third = sym(1) / sym(3) println(half + third) # exact rational arithmetic println(0.5 + 0.333333333) # floating-point approximation 5/6 0.833333333

The symbolic path gives you 5/6 — the exact answer, arrived at by finding a common denominator, not by rounding. The float path gives you a truncated decimal that was already wrong the moment you typed 0.333333333. Every symbolic result stays exact until you deliberately evaluate it numerically.

Differentiation, done symbolically

Symbolic differentiation applies the chain, product, and quotient rules to an expression tree and returns another expression tree — the exact derivative, not a finite-difference approximation. This is a categorically different thing from numerically estimating a slope: there is no step size, no truncation error, no noise.

x = sym("x") println(diff(x^3, x)) # power rule println(diff(sin(x), x)) # trig println(diff(exp(x), x)) # exponential println(diff(x^4, x, 2)) # second derivative 3*x^2 cos(x) exp(x) 12*x^2

diff(f, x, n) differentiates n times. The result is a formula you can substitute into, plot, or hand to a solver — which is exactly what you do to find critical points.

Solving equations

solve(f, x) finds the values of x that make an expression zero — for polynomials, all the roots, real and complex, exactly.

x = sym("x") println(solve(x^2 - 4, x)) # x^2 - 4 = 0 println(solve(x^2 + x - 6, x)) # x^2 + x - 6 = 0 [-2, 2] [-3, 2]

Chain differentiation and solving together and you have a complete calculus workflow — find the critical points of a function by differentiating it and solving for where the derivative vanishes:

x = sym("x") f = x^3 - 3*x^2 + 2*x df = diff(f, x) println(df) # 3*x^2 - 6*x + 2 println(solve(df, x)) # the two critical points, exactly println(diff(f, x, 2)) # second derivative for concavity

The critical points come back as exact expressions involving √3, not decimal approximations — the kind of answer you'd write on a whiteboard, produced in a browser tab.

Expanding and simplifying

The two workhorse rewrites are expand, which multiplies everything out into polynomial form, and simplify, which pulls in the other direction — combining like terms, canceling common factors, and applying identities.

x = sym("x") println(expand((x + 1)^3)) # multiply out println(simplify(x^2 / x)) # cancel println(simplify(sin(x)^2 + cos(x)^2)) # trig identity x^3 + 3*x^2 + 3*x + 1 x 1

That last line is the whole pitch in miniature. sin(x)² + cos(x)² is identically 1 for every x, and symwa knows it — not by sampling the function at a thousand points and noticing the sum stays near 1.0, but by applying the Pythagorean identity to the expression tree. A numeric engine can only ever approximate that; a CAS states it.

There's also subs for substitution — evaluating or transforming an expression by replacing a variable — and taylor for series expansions, so you can turn exp(x) into its polynomial approximation to any number of terms. The full set is walked through in the symbolic tutorial, which runs without an account.

Why a CAS in a numeric notebook

The pure-numeric browser tools — the ones that reimplement a slice of NumPy in JavaScript — stop at the float. They can integrate an ODE and multiply a matrix, but ask them for the derivative of a formula or the exact roots of a polynomial and there's nothing to answer with. Having a real CAS in the same environment closes that gap. You can derive the Jacobian of a system symbolically, subs in your operating point, and hand the exact result to a numeric solver — deriving what should be exact and computing what must be approximate, without leaving the notebook or switching languages.

It is the same architectural bet that runs through the rest of Equana: take a serious compiled library, compile it to WebAssembly, wrap it in a clean API, and run it on-device so your work never leaves your machine. If you know SymPy, symwa will feel familiar — sym, diff, solve, expand, simplify, subs, taylor are the same verbs — the difference is that this one runs in a browser tab with nothing to install.

symwa ships as part of the Equana language runtime, and the notebook is free to try at equana.dev. For more on why exactness versus approximation is a design choice and not an accident, read what quad precision actually means; to see the visualization side of the same toolkit, read volume rendering with VTK.js.

Next: Writing a language server for your own language