-
Notifications
You must be signed in to change notification settings - Fork 1
Architecture Overview
This document provides a deep-dive into the Asynkron.JsEngine execution model.
The engine has two execution paths:
- IR Execution (primary) - AST is lowered to a flat instruction sequence with explicit jumps
-
AST Walking (fallback) - Direct recursive evaluation for functions with
with/eval
flowchart LR
subgraph Input
JS[/"JavaScript Source"/]
end
subgraph Parsing
JS --> Parser((Parser))
Parser --> AST[/"AST"/]
end
subgraph Analysis
AST --> SA((Scope Analysis))
SA --> AAST[/"Annotated AST<br/>(slots, scopes)"/]
end
subgraph Execution
AAST --> Decision{Has with/eval?}
Decision -->|No| IR[["IR Execution<br/>ExecutionPlanRunner"]]
Decision -->|Yes| Walk[["AST Walking<br/>Recursive Eval"]]
end
subgraph Output
IR --> Result[/"JsValue Result"/]
Walk --> Result
end
File: Ast/AstCache.cs
AST nodes cache computed metadata to avoid repeated work using thread-safe lazy initialization:
internal static TCache GetOrCreate<TCache>(ref TCache? field, Func<TCache> factory)
{
var existing = Volatile.Read(ref field);
if (existing is not null) return existing;
var created = factory();
var prior = Interlocked.CompareExchange(ref field, created, null);
return prior ?? created;
}Cached data includes:
-
HoistPlan- variable hoisting analysis -
HoistableDeclarationsPlan- function declarations to hoist -
ExecutionPlan- lowered IR for generators/async
Files: Ast/*Extensions.cs
Functions with with statements or direct eval use recursive evaluation:
internal static JsValue EvaluateForJsValue(
this BlockStatement block,
ref JsEnvironment environment,
EvaluationContext context)
{
foreach (var statement in block.Body)
{
var result = statement.EvaluateForJsValue(ref environment, context);
// handle completion signals...
}
return result;
}File: CompletionSignals.cs
Control flow (return, break, continue, throw, yield) is modeled as typed signals rather than exceptions:
interface ICompletionSignal { }
record BreakCompletionSignal(Symbol? Label) : ICompletionSignal;
record ContinueCompletionSignal(Symbol? Label) : ICompletionSignal;
class ReturnCompletionSignal(JsValue value) : ICompletionSignal;
class ThrowFlowCompletionSignal(JsValue value) : ICompletionSignal;
class YieldCompletionSignal(JsValue value) : ICompletionSignal;
class PendingAwaitCompletionSignal : ICompletionSignal;Why signals instead of exceptions:
- Signals are faster than throwing/catching exceptions
- They carry typed data (the return value, label, etc.)
- AST walkers check for signals after each statement and propagate them
Pattern in evaluators:
var result = child.EvaluateForJsValue(ref env, ctx);
if (ctx.CompletionSignal is ReturnCompletionSignal ret)
return ret.JsValue;
if (ctx.CompletionSignal is BreakCompletionSignal { Label: null })
break;Files: Execution/ExecutionPlanBuilder.cs, Execution/Emitters/*.cs
Lowering transforms nested AST into a linear instruction stream with explicit jumps:
flowchart TB
subgraph AST["AST (nested tree)"]
IF((if))
IF --> COND((x))
IF --> THEN((then))
IF --> ELSE((else))
THEN --> A((a))
ELSE --> B((b))
end
subgraph IR["IR (linear instructions)"]
I0["0: Branch(x, then=1, else=3)"]
I1["1: Statement(a())"]
I2["2: Jump(4)"]
I3["3: Statement(b())"]
I4["4: ..."]
I0 --> I1
I0 -.->|false| I3
I1 --> I2
I2 -.->|jump| I4
I3 --> I4
end
AST ==>|"Lowering"| IR
Emitters handle specific AST constructs:
| Emitter | Handles |
|---|---|
LoopEmitter |
for/while/do-while loops |
ForOfEmitter |
for-of/for-in iteration |
TryEmitter |
try/catch/finally |
SwitchEmitter |
switch statements |
BlockEmitter |
block scopes |
YieldEmitter |
yield/yield* |
File: Execution/ExecutionPlan.cs
The result of lowering contains:
record ExecutionPlan(
ImmutableArray<ExecutionInstruction> Instructions,
int EntryPoint,
int SlotCount,
ImmutableArray<Symbol> SlotSymbols,
int RootSlotCount,
ImmutableDictionary<Symbol, int>? RootSlotMap,
int FlatSlotCount,
ImmutableDictionary<int, ImmutableArray<(int SlotIndex, int FlatSlotId)>>? FlatSlotMappings
);File: Execution/Instructions/InstructionKind.cs
Enables fast dispatch (jump table) instead of type pattern matching:
enum InstructionKind : byte {
// Statements & Expressions
Statement, Throw, EvaluateAndDiscard, BinaryOp,
IncrementSlot, CompoundAssignmentSlot,
// Scopes
PushEnvironment, PopEnvironment,
// Generator/Async
Yield, YieldStar, StoreResumeValue,
// Exception Handling
EnterTry, EnterCatch, LeaveTry, EndFinally,
// Loops
IteratorInit, IteratorMoveNext, IteratorClose,
// Control Flow
Jump, Branch, Break, Continue, Return,
// ...40+ instruction types total
}File: Ast/TypedAstEvaluator.ExecutionPlanRunner.cs
The interpreter for IR. Key fields:
private sealed partial class ExecutionPlanRunner
{
private readonly ExecutionPlan? _plan;
private int _programCounter;
private GeneratorState _state = GeneratorState.Start;
private JsVariable[]? _flatSlots; // O(1) variable storage
// Lazy-allocated state
private TryCatchState? _tryCatchState;
private AsyncState AsyncStateRef;
private YieldState YieldStateRef;
}while ((uint)_programCounter < (uint)instructionsLength)
{
var instruction = instructions[_programCounter];
var handler = InstructionHandlers[(int)instruction.Kind];
var result = handler(this, instruction, ref environment, context, out returnValue);
switch (result) {
case InstructionResult.Continue:
_programCounter = instruction.Next;
break;
case InstructionResult.Return:
return returnValue;
}
}Handlers stored in delegate array indexed by InstructionKind:
private static readonly InstructionHandler[] InstructionHandlers = InitializeHandlers();
private static InstructionHandler[] InitializeHandlers()
{
var handlers = new InstructionHandler[40];
handlers[(int)InstructionKind.Statement] = HandleStatement;
handlers[(int)InstructionKind.IncrementSlot] = HandleIncrementSlot;
handlers[(int)InstructionKind.Jump] = HandleJump;
// ...
return handlers;
}Jump and Branch are checked before dispatch table lookup:
// HOT PATH: Jump (simplest instruction)
if (instructionKind == InstructionKind.Jump)
{
_programCounter = instruction.TargetIndex;
continue; // Skip dispatch table lookup
}
// HOT PATH: Branch (conditional jump)
if (instructionKind == InstructionKind.Branch)
{
var result = HandleBranchFastPath(...);
if (result == InstructionResult.Return) return returnValue;
continue;
}
// All other instructions via dispatch table
var loopResult = InstructionHandlers[(int)instructionKind](...);| Type | File | Description |
|---|---|---|
| Sync | SyncFunctionInvoker.cs |
Normal functions, uses IR with AST fallback |
| Generator | SyncGeneratorInvoker.cs |
Pause/resume via yield, returns iterator |
| Async | AsyncFunctionInvoker.cs |
Reuses generator IR, driven internally |
| Async Generator | AsyncGeneratorInvoker.cs |
Iterator where .next() returns Promise |
flowchart LR
subgraph Sync["Sync Function"]
S1((Invoke)) --> S2((Execute IR))
S2 --> S3((Return JsValue))
end
subgraph Gen["Generator"]
G1((Invoke)) --> G2((Create Iterator))
G2 --> G3((".next()"))
G3 --> G4((Execute until yield))
G4 --> G5(("{value, done}"))
G5 -.->|"not done"| G3
end
subgraph Async["Async Function"]
A1((Invoke)) --> A2((Return Promise))
A2 --> A3((Execute IR))
A3 --> A4{await?}
A4 -->|"resolved"| A3
A4 -->|"pending"| A5((Suspend))
A5 -.->|".then()"| A3
A3 --> A6((Resolve Promise))
end
Uses IR execution by default, falls back to AST walking for with/eval:
SyncFunctionInvoker.Invoke()
-> Try IR: ExecutionPlanRunner
-> Fallback: EvaluateBody() [AST walking]
Create iterator with .next()/.return()/.throw() methods:
var runner = CreateRunner(arguments, thisValue);
runner.Initialize(); // Sets up environment, doesn't execute
return runner.CreateGeneratorObject(); // Returns iteratorReuse generator IR but drive to completion internally:
var promise = new Promise((resolve, reject) => {
runner.Initialize();
DriveToCompletion(ResumeMode.Next, undefined, resolve, reject);
});
return promise;Combine both patterns - external iterator interface with internal await handling:
asyncGen.next(value)
-> Return Promise immediately
-> Resume ExecutionPlanRunner
-> On yield: resolve Promise with { value, done: false }
-> On await pending: attach .then(), suspend
-> On return: resolve Promise with { value, done: true }
The engine tracks completion values per ECMAScript specification:
// Sentinel Pattern: JsValue.Unit means "no value produced yet"
private JsValue _scriptCompletionValue = JsValue.Unit;
// Rules:
// 1. Script start: _scriptCompletionValue = Unit
// 2. Expression statement (e.g., 5+5;): _scriptCompletionValue = 10
// 3. At script end: if still Unit -> return undefined, else return the value
// 4. Loops/Try/Catch reset: _scriptCompletionValue = Unit on ENTERExamples:
-
eval('7; for (...) {}')returnsundefined(loop resets) -
eval('7; for (...) { 9; }')returns9(loop body updates) -
eval('try { 7; } finally { 8; }')returns7(finally discarded)
- JsValue System - Value representation
- JsEnvironment & Slots - Variable storage
- IR Execution - Instruction details
- Generators & Async - Generator mechanics