-
Notifications
You must be signed in to change notification settings - Fork 1
Developer Journey
I built a JavaScript engine. In C#. From scratch.
Why? I wanted to see if I could.
You know that feeling when you look at something and think "how hard can it be?"
That's basically my entire career. Akka.NET started as a weekend hack. Proto.Actor came from being annoyed at design decisions I made in Akka.NET.
And this? This started with three sentences typed into Codex:
"I want to create a JS execution engine using C#. It will need a js parser/grammar. I want to represent the result of the parser as S-expressions / CDR/Cons. The execution engine can then treat the code similar to lisp."
Parse JavaScript. Evaluate it like Lisp. Simple, right?
OK, fast forward 2.5 months and 3,036 commits later.
Turns out JavaScript is complicated. Who knew? (Everyone. Everyone knew.)
Nov 7, 2025 "How hard can it be?"
|
|-----> CPS for async (hard, apparently)
|-----> Generators with state machines (harder)
|-----> Test262 compliance tests (93,000 ways to be wrong)
|
Dec 2025 "Maybe we need to refactor a bit"
|-----> Source generators (because typing is boring)
|-----> Complete async rewrite (take two)
|-----> IR instructions (take three? four?)
|
Jan 2026 "It actually works now"
|-----> AST evaluators sent to /Legacy
|-----> One execution model to rule them all
- Chapter 1: Genesis - Three sentences, one Codex prompt, suddenly I'm building a JavaScript engine
- Chapter 2: CPS for Async - Continuation Passing Style, or "why is async so complicated"
- Chapter 3: Generators - yield, yield*, and the state machine that wouldn't die
- Chapter 4: Test262 - 93,000 tests to tell you you're wrong
- Chapter 5: TypedAST - Goodbye Cons cells, hello type safety
- Chapter 6: Source Generators - Making Roslyn write the boring code
- Chapter 7: Async Remake - When "good enough" isn't
- Chapter 8: IR Revolution - From three execution models to one
- Chapter 9: AST Quarantine - The old code goes to live on a farm
- Chapter 10: Human Moments - "works" commits and other signs of sanity loss
- Chapter 11: Build What You Need - If you need it, build it
| What | How Much |
|---|---|
| Total Commits | 3,036 |
| Time | ~2.5 months |
| PRs | 570+ |
| Test262 Tests | 93,000+ |
| Times I Said "This Should Work" | ∞ |
| Times It Actually Worked First Try | 3 |
At one point, we had three different execution models running at the same time:
| Code Type | How It Ran |
|---|---|
| Sync code | AST tree-walking |
| Async functions | CPS transformation |
| Generators | IR state machine |
One script could touch all three. Nice, right?
But: debugging was basically impossible. Which model was running? Good luck figuring that out.
function sync() { return 1; } // AST
async function getData() { await x; } // CPS transformed
function* gen() { yield 1; } // IR state machineSo, we unified everything into IR. Much better.
Should've done that from the start. But where's the adventure in that? :-)
People ask me why I build these things. Akka.NET, Proto.Actor, and now a JavaScript engine?
Honestly? I just want to know if I can.
There's something satisfying about taking a complex problem and wrestling it into submission. JavaScript engines are supposed to be hard. V8 took years and hundreds of engineers. SpiderMonkey is ancient and battle-tested.
And here I am with Codex and stubbornness, seeing how far we can push it.
Turns out? Pretty far.
This is my journey building a JavaScript engine. Messy, honest, and there's probably a bug in there somewhere.
//Roger