I recently shipped a small expression language called littlewing, written in TypeScript, now in production at my company. It parses and evaluates formulas like:
basePrice * (1 - discount) + seasonalBonus
Marketing stores these formulas in the database, and they get evaluated at runtime.
Nothing unusual so far – lots of companies do the same. But marketing now wants something bigger:
“We want a visual formula builder where we can create and update our pricing logic ourselves… without developers.”
Cue dramatic music.
Why I Didn’t Just Use an Existing Library
We were already using expr-eval. Great little library, but two big blockers:
- It has no concept of dates, but marketing needed formulas like:
paymentDate < NOW() + FROM_DAYS(7)
- It doesn’t expose the AST, so building a visual editor on top of it would be painful.
And since expr-eval hasn’t been updated since 2019, I decided:
If we’re going to build a drag-and-drop formula editor, we should own the language behind it.
Context: I Am Not a Compiler Engineer
I’m a self-taught developer.
No CS degree. Didn’t finish high school. I’ve been writing web apps for 13 years, but compilers were always… mysterious. Something Real Computer Scientists™ did in university.
And normally, I never would have attempted building one from scratch.
But I had something new this time:
- a clear spec
- a real business need
- and a strict deadline
So I tried an experiment:
Let’s build a production-ready parser + interpreter with AI pair programming.
The Split: What I Did vs What AI Did
A lot of people are afraid of others “reading their code and thinking they cheated”. I’ll just be upfront:
This project was absolutely accelerated by AI.
But it wasn’t “AI built everything while I drank mate.”
Here’s roughly how the work was divided:
AI Was Good At:
- generating a working lexer and parser quickly
- filling in tedious boilerplate
- writing test cases
- explaining tricky concepts (like Pratt parsing) clearly
- reminding me of edge cases I might forget
I Had To:
- design the language
- decide how ASTs should look
- make performance tradeoffs
- profile and optimize hot paths
- simplify things for future UI tooling
- rewrite and refactor a lot of internals
I didn’t want a compiler for the sake of writing a compiler. I wanted a foundation for a no-code UI.
That shaped every decision.
The Design (This Part Was Definitely Me)
1. Only one data type: number
No strings. No booleans. No types of types.
Why?
Because for pricing logic, numbers were enough. And it dramatically simplifies:
- the runtime
- the AST
- the editor UI
2. Dates are numbers
A JavaScript Date is just a number (milliseconds since epoch), so:
deadline = NOW() + FROM_DAYS(7)
works exactly like normal addition. No special cases.
3. Exposed AST from day one
If the editor is going to manipulate formulas as trees, the parser had to return an AST that was easy to work with, construct, and serialize.
4. Override system
Marketing can define default variables:
discount = 0.15
Then runtime can override them (think user-specific pricing).
Clean, predictable, obvious.
Parsing: The Part I Was Afraid Of
I’d tried reading about parsing before and bounced off hard. Every explanation pointed to dense stuff: LR grammars, shift/reduce conflicts, the Dragon Book…
I asked AI:
“Explain to me how to parse expressions with operator precedence.”
It introduced me to Pratt parsing, which blew my mind because it’s so… normal.
The whole expression parser is essentially:
function parseExpression(minPrecedence) {
let left = parsePrefix()
while (getPrecedence(peekToken()) >= minPrecedence) {
left = parseInfix(left)
}
return left
}
30 lines later, I was parsing:
- nested expressions
- arbitrary operator precedence
- unary operators
- ternaries
- function calls
I felt like someone had revealed the magic trick in a card illusion.
Tuple-Based AST Instead of Objects
Most tutorial ASTs look like:
{
type: 'BinaryOp',
left: ...
operator: '+'
right: ...
}
Which is fine, but feels heavy in a UI where you’re constantly creating and transforming nodes.
I replaced them with tuples:
[nodeType, leftNode, '+', rightNode]
Why?
- smaller
- faster
- no property names to repeat
- Pattern matching is simpler:
if (node[0] === NodeType.Binary) { ... }
And in React, building trees becomes trivial:
ast.add(left, right)
No boilerplate.
This also led to a decent chunk of performance improvements.
Performance
Out of the box, littlewing was already faster than expr-eval, but I profiled and optimized:
- cursor-based lexer (no substring allocations)
- AST visitors that avoid recursion where possible
- minimized GC churn
- simple code paths over clever abstractions
And for situations where formulas run thousands of times per request, I added optional JIT:
- AST → JavaScript function string
- compiled with
new Function(...)
Speedups:
- small formulas: ~22×
- heavy formulas: up to 97×
The trade-off is initial compilation cost. Use when evaluating repeatedly.
AI Didn’t Replace Me — It Let Me Move Faster
Let’s be real:
If I had built this on my own knowledge alone, learning everything from scratch, it probably would have taken two months.
AI compressed:
- learning curve
- boilerplate
- test scaffolding
in exchange for:
- refactoring
- decision-making
- understanding
If you treat AI like StackOverflow with autocomplete, you get garbage.
If you treat it like an intern who writes code you must review, profile, and rewrite as needed, you can move extremely fast.
What I Learned
Compilers aren’t magic
They’re mostly:
- tokenizing strings
- building trees
- walking trees
Everything else is just detail.
Don’t abstract too early
Simple code profiled well. “Clever” structures often slowed things down.
Projects get better when you know where they’re going
The reason littlewing works is because it was designed with the editor in mind before writing any code.
Using AI doesn’t make you a fraud
Not understanding your own code does.
I understand every part of littlewing because I had to:
- question it
- debug it
- rewrite it
- make it production-worthy
Try It
npm install littlewing
import { evaluate, defaultContext } from 'littlewing';
evaluate(`
late = paymentDate > (NOW() + FROM_DAYS(7))
late ? 0.05 : 0
`, {
...defaultContext,
variables: { paymentDate: Date.now() }
});
Wrapping Up
This wasn’t a project about building a compiler.
It was about giving non-technical teammates the power to build pricing logic without waiting for a developer ticket to be picked up.
AI didn’t write littlewing for me.
AI helped me ship something I wasn’t sure I was capable of building in the time I had.
And that’s not cheating — that’s using your tools.
If you want to explore the code:
- GitHub: https://github.com/brielov/littlewing
- Playground: https://littlewing-demo.vercel.app
- npm:
littlewing
If you want to build a visual formula editor or no-code logic builder, littlewing might save you some groundwork.
Happy hacking.
PS: Before anyone asks—yes, the blog post was pair-written with AI too. I’m just staying consistent with the architecture.
Top comments (2)
Pretty good read! Its nice to see how devs are using AI while building tools that actually improve their work and the company workflow, good job!
Thanks buddy! Glad you enjoyed the read (: