DEV Community

Cover image for I Needed Date Math in Formulas, So I Built a Compiler (and Learned a Lot)
Gabriel Vaquer
Gabriel Vaquer

Posted on

I Needed Date Math in Formulas, So I Built a Compiler (and Learned a Lot)

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. It has no concept of dates, but marketing needed formulas like:
paymentDate < NOW() + FROM_DAYS(7)
Enter fullscreen mode Exit fullscreen mode
  1. 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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: ...
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

Why?

  • smaller
  • faster
  • no property names to repeat
  • Pattern matching is simpler:
if (node[0] === NodeType.Binary) { ... }
Enter fullscreen mode Exit fullscreen mode

And in React, building trees becomes trivial:

ast.add(left, right)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
import { evaluate, defaultContext } from 'littlewing';

evaluate(`
  late = paymentDate > (NOW() + FROM_DAYS(7))
  late ? 0.05 : 0
`, {
  ...defaultContext,
  variables: { paymentDate: Date.now() }
});
Enter fullscreen mode Exit fullscreen mode

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:

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)

Collapse
 
adrberia profile image
Adrberia

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!

Collapse
 
brielov profile image
Gabriel Vaquer

Thanks buddy! Glad you enjoyed the read (: