Equana

All articles
July 25, 2026·Max Schuriglanguage-designequanaengineering

Why we built our own language instead of a MATLAB-compatible interpreter

The pivot from "rewrite Julia in TypeScript" to designing Equana — what a clean-sheet language bought us in granularity and backend flexibility, and what it cost.

The most common reaction I get when I say Equana runs its own language is a raised eyebrow. There's a strong prior in this space, and it's a reasonable one: don't invent a language. Match one people already know. Ship a MATLAB-compatible interpreter, or run CPython in WebAssembly like Pyodide does, and inherit a universe of existing code and muscle memory. Building a new numerical language in 2026 sounds like the kind of thing you talk yourself out of.

I built one anyway, and it was the right call. This post is the reasoning — not as a confession of sunk cost, but as an engineering decision I'd make again with the same information.

The Julia-in-TypeScript attempt

Equana didn't start as a new language. It started as an attempt to rewrite Julia in TypeScript, because Julia is the best-designed language I know for numerical work: multiple dispatch as the organizing principle, a type system built for math, 1-based indexing that matches the textbooks. If I could run that in a browser, I'd be done.

The attempt failed, and it failed for a specific, structural reason rather than a lack of effort. Julia's semantics are deeply coupled to its native compilation pipeline. Julia is fast because it JIT-compiles specialized machine code per type signature through LLVM; a huge amount of what "Julia the language" means — how dispatch specializes, how types propagate, the performance model users actually rely on — is inseparable from "Julia the LLVM-based compiler." You can't lift the language off that substrate cleanly. You're left with two bad options: ship the entire native toolchain (enormous, slow to start, and hostile to the notebook workflow of running one small cell at a time), or reimplement enough of the semantics in TypeScript that you've quietly built a different language — one that looks like Julia but is subtly incompatible in a hundred places.

That second failure mode is the one that convinced me to stop. Subtle incompatibility is worse than open incompatibility. A language that's 95% Julia invites users to bring the other 5% of their expectations, and every place you don't meet them is a papercut that feels like a bug in your product rather than a difference in a new tool. The same logic rules out a MATLAB-compatible interpreter — you'd own bug-for-bug fidelity to a spec you don't control, plus the legal weather around the .m ecosystem — and it's why Pyodide, which is genuinely excellent, still wasn't the answer: it drags a whole CPython runtime and its package ecosystem into every page load, and its numerical core was never designed around browser primitives like SharedArrayBuffer.

So the choice wasn't "clone a language" versus "invent one." Once you accept you're going to reimplement the semantics anyway, cloning gets you a language that's harder to build (you're chasing someone else's exact behavior) and worse for your constraints (you inherited design decisions made for a native compiler). At that point, designing your own is the lower-risk path, not the riskier one.

What the rewrite bought

Committing to a clean-sheet language turned a set of hard constraints into design freedoms. Four things fell out of it.

Granularity. An interpreter I designed can execute at whatever grain the notebook needs — one cell, one statement, one AST node — with full control over pause and resume. That's not a cosmetic nicety; it's what makes a real debugger with breakpoints and stepping almost free, because a breakpoint is just "pause before this node" and inspection is just "read the environment map." A borrowed runtime built for whole-program execution fights you on this at every turn. Owning the tree walk means owning the execution model.

Clear native interfaces. Because I defined the value model, I defined it to match the hardware I actually call. Array values in Equana are handles — a pointer, a dtype, a shape, and strides — which is precisely the shape of a BLAS or LAPACK argument. There's no impedance mismatch to bridge, no boxing to unwind before a kernel can run, because the language's array type is a description of memory a numerical kernel can operate on directly. A language designed around someone else's object model would need a translation layer at every native boundary.

Easy syntax. Designing the surface syntax myself meant I could aim it at the actual audience — scientists and engineers who are occasional programmers — rather than inheriting a general-purpose language's sigils. Equana is Julia-inspired where Julia is good (multiple dispatch, 1-based indexing, dynamic typing with optional annotations) and adds an optional natural-language layer on top, so let f be x mapsto x^2 and f = x -> x^2 are the same statement. You only get to make those calls if you own the grammar.

Flexible backends. This is the one that pays off most. Every numerical operation dispatches down one chain: a native implementation on the desktop build (OpenBLAS, Apple Accelerate, SuiteSparse via the Tauri app), a WebAssembly binary that runs everywhere, and a TypeScript fallback that's mandatory so nothing is ever unavailable. The same A * B in your notebook resolves to the fastest backend present on the machine it's running on, and the language doesn't change. That three-tier structure is only possible because I control dispatch and the value model end to end — the interpreter selects a backend; the backend operates on the shared-memory handle. A cloned runtime would have hardwired one execution strategy and one memory model, and this flexibility would have been off the table.

The performance story backs this up rather than undercutting it. Numerical work doesn't run in the interpreter at all — it runs in kernels. Our hand-written WebAssembly SIMD dgemm kernel reaches about 37.9 GFLOPS single-threaded at N=4000, which actually edges out OpenBLAS compiled to WASM (~30.7), and the multi-threaded WASM path hits ~130 GFLOPS at N=1000 and ~410 at N=4000 across 32 threads. The interpreter being a simple AST-walker is not a bottleneck, because it was never on the hot path by design.

What it cost

A clean-sheet language is not free. Two costs are real and permanent.

An ecosystem restart. Every language that exists has a head start measured in decades of libraries, Stack Overflow answers, and trained users. Equana starts at zero and has to earn each package and each user. Today that means 20 packages and 282 functions covering NDArrays, sparse matrices, linear algebra, FFT, statistics, plotting, and symbolic math — a real standard library, but a small fraction of what NumPy or Julia's ecosystem offers. Closing that gap is ongoing work, not a solved problem.

No .m compatibility. You cannot paste MATLAB code into Equana and run it, and you cannot import numpy. Familiarity is the mitigation — someone who knows MATLAB or Julia can read Equana on sight — but familiarity is not compatibility, and I don't claim it is. If your work is a pile of existing .m files, Equana asks you to port, not to paste.

I take those costs with open eyes because the alternative wasn't "get the ecosystem for free." The alternative was to inherit a runtime built for a different machine, fight its execution model to get notebook-grade granularity, bridge its object model at every native call, and still end up subtly incompatible with the language I was cloning. Measured against that, a new language buys control where control is exactly what a browser-native, backend-flexible, debuggable numerical notebook needs.

You don't build your own language because you think you're smarter than MATLAB or Julia. You build it when the thing you're actually trying to make — here, a notebook that runs entirely in the browser, over shared memory, with a real debugger and three interchangeable numerical backends — has requirements no existing runtime was designed to meet. Then a new language stops being an indulgence and becomes the straightforward path.

You can try it at equana.dev (alpha, free, sign-in for the full notebook). The syntax design that this decision made possible is in designing a language that reads like English, and the backend-dispatch chain that it unlocked is in same code, three backends.

Next: The compute-heavy app whose server does no computing