Skip to content

Journey 01 Genesis

Roger Johansson edited this page Jan 15, 2026 · 7 revisions

Chapter 1: Genesis

November 7, 2025 - The day I decided JavaScript engines couldn't be that hard

The Prompt

I was messing around in the OTelMCP project when the thought hit me: what if I built a JavaScript engine?

Not a wrapper around V8 or anything sensible like that. A real interpreter. From scratch. In C#.

My thinking was simple. Lisp interpreters are elegant - cons cells and recursion. JavaScript is just... syntax sugar over objects and functions, right?

So, what if we:

  1. Parse JavaScript
  2. Convert it to S-expressions
  3. Evaluate it like Lisp

Three sentences into Codex:

"I want to create a JS execution engine using C#

  1. it will need a js parser/grammar.
  2. I want to represent the result of the parser as S-expressions / CDR/Cons
  3. the execution engine can then treat the code similar to lisp"

That's it. That's the prompt that started everything.

Codex Delivers

I hit enter and waited. Codex came back with:

Summary

Introduced the new Asynkron.JsEngine library with cons-based S-expression primitives, a lexer/parser, and an evaluator so JavaScript-like code can run with Lisp-style semantics and host interop hooks.

Files: 23

23 files. One prompt. A working JavaScript engine with tests.

Nice, right?

It even built and the tests passed. The original was called JsLispEngine and used Esprima for parsing:

src/Asynkron.JsEngine/
├── Parsing/JsSExpressionBuilder.cs  ← Esprima AST → S-expressions
├── Runtime/JsLispEngine.cs          ← The original evaluator
├── SExpressions/SExpression.cs      ← Cons, Symbol, Nil
└── context.md

But: I wanted more control. Esprima is nice but it's someone else's parser. If I'm gonna build an engine, I want to understand every piece of it.

The Rewrite

So, I rewrote it. Threw out Esprima, wrote my own lexer and parser.

Codex Version My Version
Esprima (external) Handwritten Lexer.cs/Parser.cs
JsLispEngine class Evaluator static class
SExpression.cs Split: Cons.cs, Symbol.cs
Basic objects JsObject, JsArray, JsFunction
No control flow BreakSignal, ContinueSignal, etc.

The "first" commit landed with 4,270 lines across 26 files. All working. All tested.

The Lisp Inside

Here's the core idea that still makes me happy about this design.

JavaScript like this:

let x = 1 + 2;

Becomes an S-expression like this:

(let x (+ 1 2))

Which is basically just cons cells:

Cons(Symbol("let"),
  Cons(Symbol("x"),
    Cons(Cons(Symbol("+"), Cons(1, Cons(2, Nil))), Nil)))

And the evaluator? It's basically just a Lisp eval:

public object Evaluate(object expr, Environment env)
{
    if (expr is double or string or bool)
        return expr;  // Self-evaluating

    if (expr is Symbol sym)
        return env.Lookup(sym.Name);  // Variable lookup

    if (expr is Cons cons)
    {
        var head = cons.Head;

        if (head == JsSymbols.Let)
            return EvaluateLet(cons, env);
        if (head == JsSymbols.If)
            return EvaluateIf(cons, env);
        if (head == JsSymbols.Add)
            return EvaluateAdd(cons, env);

        return EvaluateCall(cons, env);  // Function call
    }

    throw new Exception($"What is this: {expr}");
}

Pattern match on the head symbol, dispatch to the right handler. Elegant.

Control Flow via Exceptions

This is either clever or stupid, depending on who you ask.

I used exceptions for control flow:

public class BreakSignal : Exception { }
public class ContinueSignal : Exception { }
public class ReturnSignal : Exception
{
    public object Value { get; }
}

When you hit a return statement, throw a ReturnSignal. The function catches it and extracts the value. Same for break and continue in loops.

Is it idiomatic C#? No. Does it work? Yes. Did I change it later? Also yes. :-)

The Test Suite

From day one: 932 tests. 565 for the evaluator, 367 for the parser.

I've learned the hard way (Akka.NET, Proto.Actor) that tests are everything. When you refactor, tests tell you what you broke. When you're exhausted at 2am, tests tell you to go to bed because nothing passes anyway.

What Came Next

Within hours of that "first" commit, the copilot-swe-agent started hammering out PRs:

PR What
#1 README, solution structure
#2 Ternary operator, template literals, getters/setters
#3 Spread/rest operators
#4 Array methods, Math object
#5 Date, JSON objects
#6-7 CPS transformer (here we go...)

The foundation was solid. The Lisp idea was working.

And I had no idea how complicated things were about to get.


Next: Chapter 2: CPS for Async - Where I learn that async/await is not "just syntax sugar"

//Roger

Clone this wiki locally