Reference case

JsEngine shows the hard parts.

A 100% AI-generated JavaScript engine in C#, pushed through ECMAScript 262 semantics, statement IR, expression bytecode, async execution, generators, promises, realms, slots, and roughly 94,000 Test262 cases with 99.5% success.

Architecture walkthrough

A JavaScript engine is a pile of sharp edges.

The central design is tiered: parse JavaScript into a typed AST, stamp scope and slot metadata onto that tree, lower control flow into statement IR, and compile many expression payloads into stack-based bytecode programs.

Front end
ParserSource becomes typed program, statement, and expression nodes rather than opaque syntax strings.
Scope analysisHoisting, TDZ, block scope, closure capture, and identifier metadata are calculated before execution.
AST cacheNodes cache hoist plans, declaration plans, execution plans, and shape metadata with thread-safe lazy initialization.
LoweringEmitters flatten loops, try/catch, switch, for-of, for-in, blocks, and yield into explicit instructions.
Execution
Statement IRExecutionPlanRunner executes a linear instruction stream with a program counter and fast dispatch.
Expression bytecodeExpressionProgram lowers values, calls, jumps, property access, object creation, super, and private-field operations.
Completion signalsReturn, break, continue, throw, yield, and await are represented as typed flow state.
Function variantsSync, generator, async, and async generator functions all ride the same execution machinery differently.
Runtime
ValuesJsValue is the tagged value boundary for undefined, null, numbers, strings, symbols, objects, and host values.
ObjectsDescriptors, prototype chains, extensibility, exotic objects, proxies, typed arrays, and iterators are modeled directly.
EnvironmentsLexical scope is stored in slots, with flat slot mappings for hot execution paths.
JobsPromises, await, queueMicrotask, and module loading interact through an epoch-aware microtask queue.
IR and bytecode

The engine does not just walk syntax.

Most JavaScript runs through two lowered layers: statement IR for control flow, and expression bytecode for the values inside returns, throws, declarations, assignments, branches, loops, yields, class fields, and iterator setup.

01SourceJavaScript enters as source text, with strictness, module mode, host hooks, and timeout state attached.
02Typed ASTParsing creates expression and statement nodes with source references for diagnostics.
03Scope stampIdentifiers receive scope depth, slot index, scope id, and optional flat slot id.
04LowerStatements become an ExecutionPlan; expression operands become ExpressionProgram bytecode where supported.
05RunThe runner executes instructions and evaluates embedded expression programs until return, throw, yield, await suspension, or completion.

AST boundary

  • The AST remains the source of truth for parsing, scope stamping, caches, source references, and semantic shape.
  • Legacy expression evaluation is quarantined; tests scan runtime code to catch accidental fallback.
  • Dynamic features such as direct eval and with are treated as semantic boundaries, not hand-waved away.

Statement IR

  • Turns control flow into instruction indexes and program-counter jumps.
  • Has specialized instructions for loops, try/finally, iterators, yields, variable access, operators, and completion handling.
  • Gives generators and async functions resumable state without replaying the whole AST.

Expression bytecode

  • ExpressionProgramCompiler lowers expression trees into compact stack programs with literal, string, object, identifier, and spread-mask constant pools.
  • Bytecode covers literals, identifiers, calls, constructors, object and array creation, property get/set/update, short-circuit jumps, super access, and private-field checks.
  • IR instructions carry programs such as ReturnProgram, InitializerProgram, ConditionProgram, YieldProgram, and AwaitedProgram instead of expression AST payloads.
Mechanics diagrams

The interesting parts are state machines.

These diagrams show the engine as cooperating machines: lowering turns syntax into two execution layers, async work moves through the microtask queue, and lexical memory is carried through slots, flat indexes, and pooled environments.

Await and microtasks

Scheduling model
sequenceDiagram
  participant JS as JS code
  participant Runner as Runner
  participant Await as Await op
  participant Promise as Promise
  participant Queue as Microtask queue
  participant Resume as Resume callback

  JS->>Runner: enter async function
  Runner->>Await: evaluate AwaitedProgram
  Await->>Promise: observe promise-like value
  alt already settled
    Promise-->>Runner: resolved value
  else pending
    Await->>Queue: attach pooled resume callback
    Runner-->>JS: return outer Promise
    Promise->>Queue: enqueue reaction job
    Queue->>Resume: drain permitted epoch
    Resume->>Runner: continue at saved pc
  end
  Runner-->>JS: resolve or reject outer Promise
              

Generator states

Pause and resume
stateDiagram-v2
  [*] --> SuspendedStart
  SuspendedStart --> Executing: next(value)
  Executing --> SuspendedYield: yield or yield*
  SuspendedYield --> Executing: next(value)
  SuspendedYield --> Closing: return(value)
  SuspendedYield --> Throwing: throw(error)
  Throwing --> Executing: run finally
  Closing --> Executing: run finally
  Executing --> Completed: return or end
  Completed --> [*]
              

The diagrams are rendered from Mermaid source in the page, so the architecture can keep evolving with the implementation instead of becoming a stale exported image.

Memory and context

Scope is where JavaScript gets expensive.

The engine treats lexical environments as pooled runtime objects with slot arrays. Variables can be found through named lookup, direct slot access, or flat slots that collapse cross-scope access into one indexed array for IR hot paths.

Named lookupScope chain
Slot lookupO(1)
Flat slotsHot path
PoolingCaptured-safe
TDZSlot flag
ImportsSpecial binding
ClosuresCaptured env
TimeoutsCancellation

Execution context

  • EvaluationContext carries realm state, completion state, cancellation, scope stack, async pending state, and script completion value.
  • Control flow is data, not uncontrolled exception unwinding: return, throw, yield, break, continue, and pending await are inspected by the runner.
  • That makes it possible to implement JavaScript's awkward completion value rules for eval, loops, try/finally, and modules.

Environment storage

  • JsEnvironment owns a slot array with name, value, and packed flags such as const, lexical, uninitialized, and immutable binding.
  • Closure capture marks environments so the pool will not reclaim live lexical state.
  • Flat slot mappings let statement IR and expression bytecode bypass repeated scope-chain traversal in loops and arithmetic-heavy code.

Runtime allocation

  • Slot arrays use bucketed pooling; environments, iterators, microtasks, and callback wrappers use rent/return patterns.
  • Debug guards catch incorrect reuse in development while keeping release paths lean.
  • This matters because 94,000 compliance tests magnify every tiny allocation leak or captured-state mistake.
Event loop

Promises turn execution into scheduling.

Promises implement microtasks. When a promise settles it schedules handler processing on the engine queue, tagged with an epoch so module loading and forced drains can decide exactly which work is allowed to run.

Promise jobs
StatePending, fulfilled, and rejected states transition once, then schedule handler processing.
Handlersthen, catch, and finally chains create follow-on promises and enqueue callbacks.
MicrotasksSettled promises and queueMicrotask callbacks share the same engine queue.
EpochsSelective draining preserves later module jobs while running the current safe batch.
Resumable code
Generatorsyield pauses the runner and StoreResumeValue consumes next, return, or throw payloads.
yield*Delegation forwards next, return, and throw into an inner iterator while preserving cleanup semantics.
Async functionsReturn a promise immediately, then drive the same runner internally until completion or pending await.
Async generatorsExpose an async iterator where each step returns a promise that resolves on yield or completion.

Why this is hard

  • await on a settled promise should continue quickly, but pending promises must suspend without blocking the host.
  • for await...of has to bridge sync iterables, async iterables, iterator closing, exceptions, and loop-scoped variables.
  • generator.return() and generator.throw() must run finally blocks, close active iterators, and resume at the right instruction.
  • Async generator .next() returns a promise, so iterator results are no longer direct values.
01

Promise settles and schedules itself as a microtask through the engine queue.

02

Drain logic executes only permitted epochs and defers later jobs instead of losing ordering.

03

Await handling either extracts a settled result or attaches pooled resume callbacks to continue the runner later.

04

Generator state stores the program counter, resume payload, try/finally state, delegated iterator state, and pending async promise.

Why the case works

This is complexity, not just volume.

Asynkron.JsEngine is an unusually good reference case because correctness is externally judged by ECMAScript semantics and Test262, not by whether a small app appears to work in a happy path.

Specification pressure

  • Language semantics include hoisting, TDZ, closures, direct eval, with, classes, modules, destructuring, iterators, exceptions, and completion values.
  • Built-ins force exact property descriptors, prototype behavior, realm-sensitive constructors, typed arrays, RegExp, BigInt, Temporal, and Symbol protocols.
  • Test262 finds the corners that broad manual QA almost never reaches.

Agent-shaped delivery

  • Work can be split into precise semantic slices: one instruction handler, one built-in method, one lowering rule, one regression pack.
  • The suite gives immediate truth: a fix either moves failing cases, preserves regressions, and avoids host crashes, or it does not.
  • Investigation notes and todo files become durable project memory for the next agent pass.

Architectural lessons

  • Correctness demanded layered execution: AST metadata, statement IR, expression bytecode, and explicit dynamic-scope boundaries.
  • Memory safety meant distinguishing pooled, returned, and captured state in the runtime itself.
  • Async behavior required a real scheduling model, not just C# tasks glued onto JavaScript promises.

The point of this case is not that a JavaScript engine is a normal business backlog item. The point is sharper: Faktorial can work in domains where the acceptance criteria are deep, external, and adversarial.