This is the second post in my "My Open-Source Projects" series, where I go through some of the OSS projects I started or maintain and tell you the story behind them. Last time it was AngleSharp - the HTML parser that started life as a plane-ride fever dream. This time: MAGES, a much smaller project with a surprisingly cool client list.
What Is MAGES, Actually?
MAGES - officially standing for "Mages: Another Generalized Expression Simplifier," because every good project needs a slightly forced backronym - is a small, fast expression parser and interpreter for .NET. You hand it a string like "sin(2) * cos(pi / 4)", it hands you back a number. Except it also does variables, functions, closures, objects, lists, complex numbers, and - as of a couple of years ago - a bit of JSX for fun.
Think of it as "give your .NET app a tiny, embeddable scripting language" without dragging in a full-blown runtime, a NuGet dependency tree the size of a phone book, or a licensing conversation.
var engine = new Mages.Core.Engine();
var result = engine.Interpret("sin(2) * cos(pi / 4)"); // 0.642970376623918
That's the whole "hello world." No config, no ceremony.
The YAMP Prehistory
MAGES didn't start as MAGES. It started as YAMP - "Yet Another Math Parser" - an experiment to see whether I could build a full parser and evaluator with MATLAB-like syntax, powered almost entirely by reflection. Reflection made it wonderfully easy to extend: want a new function? Slap an attribute on a method and YAMP would pick it up. It was never meant to be fast. It was meant to be flexible, and as far as experiments go, it worked.
YAMP ended up seeing more real usage than an "experiment" really has any right to. The best example: it powered the math engine behind SineRider, a Unity web game from 2013 where you graph functions to sled a character through checkpoints - think Line Rider, but the slope comes from an equation you type in, not a mouse drag. It's a delightful bit of edutainment, and knowing that YAMP's reflection-driven, definitely-not-optimized-for-speed parser was quietly evaluating someone's sine wave in a browser game is exactly the kind of thing that makes open source fun.
If you want to see what that original version actually looked like, there's footage of it right here - very much a product of its era. The good news is you don't have to rely on old footage: after the original Unity Web Player build was killed off by browsers dropping NPAPI support, a team of teenagers at Hack Club rebuilt SineRider from scratch in vanilla JavaScript, and it's still there, still free, still very playable today. Current sources live at hackclub/sinerider if you're curious how it's built these days (spoiler: no MAGES or YAMP in there anymore - it's its own thing now).
From Experiment to Grant-Funded Successor
Here's where the story gets a step more interesting than "I wrote a parser on a plane" (that's a different article; see first part of the series). The person behind SineRider wanted to go bigger: not just a game about graphing one function, but a whole world built on the idea that everything - terrain, objects, physics - could be governed by changeable math functions. Picture Minecraft, except the blocks are mutable equations. That's a genuinely ambitious pitch, and it was ambitious enough to catch a Peter Thiel grant.
Part of that grant went toward funding the next generation of the expression engine underneath it. YAMP's syntax could carry over, but its reflection-based guts were never going to be fast enough for a real-time, function-driven game world. So MAGES was built from scratch with one primary constraint: speed.
How MAGES Actually Runs Your Expression
The core architectural decision that makes MAGES fast is that it doesn't walk an AST and evaluate it node-by-node at runtime (which is what YAMP effectively did, dressed up with reflection). Instead, every expression gets compiled down into a small set of VM instructions, which are then executed sequentially by a lightweight stack-based virtual machine. Parse once, compile once, run the compact bytecode as many times as you want.
Because the VM is just consuming a flat instruction stream, the interesting side effect is that the front end (parser, AST) and back end (what actually executes the instructions) are decoupled. Theoretically you could target something other than the built-in VM. I actually did this for fun once: I wrote a backend that "compiles" MAGES code into equivalent JavaScript and runs it on V8. It works, and it was a satisfying afternoon of yak-shaving. The more natural target would honestly be WebAssembly - same idea, different, more useful destination - but that one's still sitting in the "would be cool" pile.
// Compile once, reuse the compiled delegate as many times as you like
var expOne = engine.Compile("exp(1)");
var result = expOne(); // 2.71828182845905
Getting Started
Install it:
dotnet add package Mages
Interpret a one-off expression:
var engine = new Mages.Core.Engine();
var result = engine.Interpret("sin(2) * cos(pi / 4)"); // 0.642970376623918
Pull a function back out and call it directly from C#:
var func = engine.Interpret("(x, y) => x * y + 3 * sqrt(x)") as Mages.Core.Function;
var result = func.Call(4, 3); // 18.0
Since v2.0, complex numbers are first-class citizens - this was a hard requirement to reach parity with what YAMP already offered, since a fair chunk of MAGES's real-world users do actual signal-processing-flavored math and were not going to be pleased with an engine that couldn't take a square root of -1 without complaining:
var result = engine.Interpret("sqrt(-1)"); // i
And since v3.0 (2024), things got a little playful: MAGES understands JSX. Not "JSX-inspired" or "JSX-ish" - the actual JSX grammar, the same tags-with-embedded-expression-blocks syntax React (or Vue, or pretty much any modern web toolchain) uses. The tag structure, attributes, and children all follow the standard JSX rules you already know. The only twist is what happens inside the curly-brace expression blocks: instead of that being JavaScript, it's MAGES doing the evaluating. Same shape, different engine running the logic.
var html = engine.Interpret(
"<div class={\"hello\" + \",\" + \"there\"}><h1>Hi</h1><p>World.</p></div> | html"
);
// <div class="hello,there"><h1>Hi</h1><p>World.</p></div>
So {"hello" + "," + "there"} there isn't JavaScript string concatenation - it's a MAGES expression, evaluated by the same stack-based VM as everything else, just sitting inside a JSX attribute the way you'd expect from any React component. Pipe the whole thing through html and you get a serialized string back. It's a genuinely fun corner of the language: you get a templating syntax that looks completely familiar to anyone who's touched a modern frontend, backed by an evaluator that has nothing to do with a JS engine.
Reflect on objects and functions at runtime via type:
var meta = engine.Interpret("((x, y, z) => x + y + z) | type | json");
// { "name": "Function", "create": "[Function]",
// "parameters": { "0": "x", "1": "y", "2": "z" } }
And use placeholders (_) to curry arguments through the pipe operator:
var result = engine.Interpret(@"
var f = (x, y, z) => x + 2 * y + 3 * z;
5 | f(1, _, 2)
"); // 17, computed as 1 + 2*5 + 3*2
That pipe-and-placeholder combo is one of my favorite bits of syntax in the whole language - it reads almost like a small shell pipeline, but it's fully typed C# objects flowing through.
Where It Shines
-
Small and dependency-free. MAGES itself has zero runtime dependencies. It's a
netstandard2.1library, which means it happily runs on things like Unity or Mono, not just modern .NET. - Fast by construction. Compiling to a flat instruction stream for a stack-based VM means repeated evaluation is cheap - exactly what you want if you're evaluating the same handful of user-authored formulas thousands of times a second (looking at you, real-time math games and signal processing tools).
- A genuinely pleasant expression syntax. Closures, objects, lists, complex numbers, string interpolation, a pipe operator, and now JSX-flavored templating - it covers far more ground than "just arithmetic."
-
Easy to embed.
new Engine(), call.Interpret()or.Compile(), done. No hosting ceremony. - Great for domain-specific math. If your app's users need to type in formulas - a measurement tool, a spreadsheet-like calculator, a scripting console - MAGES gives you that without writing your own parser (please don't write your own parser).
Where It Struggles
- It's a niche tool by nature. It's not going to replace a general-purpose scripting language embedded in your app if you need full standard-library breadth, multi-file modules, or a package ecosystem. It's an expression/formula engine that grew some scripting-language features, not the other way around.
- The (optional) type system doesn't exist yet. More on this below, but right now everything is dynamically typed, and if you pass the wrong shape of data into a compiled expression, you find out at runtime like it's 2005.
- Tooling is thin. There's no language server, no syntax highlighting extension, no step-through debugger. If you're building something where non-developers will be writing MAGES expressions, you're currently on your own for the authoring experience.
- Smaller community, smaller surface area for stuff to just already exist. Compared to AngleSharp's few-hundred-million downloads, MAGES sits at a comparatively modest ~200K total NuGet downloads. That's not a criticism of the project so much as an honest expectation-setter: this is a focused tool for a specific job, not an ecosystem.
Small Crew, Big Reach
MAGES has never had a huge contributor list - it's a much more focused, much smaller codebase than something like AngleSharp. But it has picked up real sponsors along the way, credited right in the README: polytroper (the studio behind that "SineRider, but the whole world is math" follow-up game I mentioned earlier), smapiot (a company I previously worked for), and OMICRON Lab.
That last one deserves its own section.
Where MAGES Shows Up Today
For a project with a comparatively small download count, MAGES keeps turning up in places that make me genuinely proud:
- Microsoft PowerToys uses MAGES - it's listed directly in the project's own README as one of its notable consumers. Every time someone runs a calculation through a PowerToys utility, there's a decent chance MAGES is quietly doing the arithmetic.
- Flow Launcher, the 15K+ star Windows app launcher, depends on MAGES too - a natural fit, since "type an expression into a launcher and get an instant answer" is exactly the kind of thing MAGES was built for.
- OMICRON Lab, an Austrian company building programmable test-and-measurement gear, uses MAGES as the expression engine behind their instruments. This is the one I'm most proud of: their hardware runs user-programmable math over live measurement data, which means MAGES had to be fast and correct with complex numbers - no hand-waving allowed when someone's oscilloscope trace depends on getting the math right. This is also, not coincidentally, exactly why complex number support in v2.0 was a "must ship," not a "nice to have."
- LisaCore builds dynamic runtime code execution on top of it.
None of this was the plan when MAGES was a grant-funded rewrite of a reflection-based math parser. But "a small, fast, embeddable expression engine" turns out to be one of those quietly universal building blocks - much like AngleSharp's "a proper HTML parser for .NET" from the last post in this series.
What's Next for MAGES
A few directions I keep circling back to:
- Real language tooling. The dream is an actual Language Server Protocol (LSP) implementation with a companion VS Code extension - proper syntax highlighting, inline diagnostics, maybe even autocomplete for the built-in function library. Right now, writing MAGES expressions means writing them blind.
- Integration into a new .NET web framework. There are some interesting possibilities for using MAGES as a lightweight expression layer inside web tooling - think templated logic without pulling in a full scripting runtime. Nothing concrete to announce, but it's on the table.
- Optional types. This is the big one, and it's a lot harder than it looks. Adding syntax for type annotations is the easy 10%. The actual hard part is making type checking meaningful - catching real errors before runtime, without turning simple one-line formulas into a TypeScript-style compiler project, and producing error messages that are actually useful rather than a wall of "type mismatch at position 47." I don't have a timeline for this. I have opinions about it, which is a different thing entirely.
That's the MAGES story - from a reflection-powered math experiment riding along in a Unity sledding game, to a Thiel-grant-funded rewrite, to quietly doing arithmetic inside Microsoft PowerToys and Austrian test equipment. If you want to poke around, the code lives on GitHub, and the package is on NuGet.
Next up in this series: another project, another origin story. Stay tuned.








Top comments (0)