Skip to content

IR Execution

Roger Johansson edited this page Jan 14, 2026 · 2 revisions

IR Execution

Detailed documentation of the Intermediate Representation (IR) execution system.


Instruction Types

File: Execution/Instructions/InstructionKind.cs

Complete instruction taxonomy (40+ types):

internal enum InstructionKind : byte
{
    // Statements & Expressions
    Statement,                          // Evaluate statement node
    Throw,                              // Throw expression
    EvaluateAndDiscard,                 // Evaluate expression, discard result
    BinaryOp,                           // Fast-path binary operation
    IncrementSlot,                      // ++/-- on slot variable
    CompoundAssignmentSlot,             // += -= *= etc on slot

    // Variables & Functions
    SimpleVariableDeclaration,          // let x = expr (no destructuring)
    ComplexVariableDeclaration,         // let {a, b} = obj (destructuring)
    FunctionDeclaration,                // function f() {}
    ClassDeclaration,                   // class C {}

    // Scopes
    PushEnvironment,                    // Enter new scope
    PopEnvironment,                     // Exit scope

    // Control Flow
    Jump,                               // Unconditional jump
    Branch,                             // Conditional jump
    Break,                              // Break statement
    Continue,                           // Continue statement
    Return,                             // Return statement

    // Generator/Async
    Yield,                              // yield expression
    YieldStar,                          // yield* expression
    StoreResumeValue,                   // Store .next(value) payload

    // Exception Handling
    EnterTry,                           // try { ... }
    EnterCatch,                         // catch (e) { ... }
    EnterCatchWithDestructuring,        // catch ({x}) { ... }
    LeaveTry,                           // Normal completion of try/catch
    EndFinally,                         // finally { ... }

    // Loops
    BreakableEnter,                     // Enter loop/switch
    BreakableExit,                      // Exit loop/switch
    IteratorInit,                       // for-of initialization
    IteratorMoveNext,                   // for-of iteration step
    IteratorClose,                      // for-of cleanup

    // For-In Loops
    ForInInit,                          // for-in initialization
    ForInMoveNext,                      // for-in iteration step

    // With Statements
    EnterWith,                          // with (obj) { ... }
    LeaveWith,                          // Exit with scope

    // Array Destructuring
    ArrayDestructuringInit,             // Initialize iterator
    ArrayDestructuringElement,          // Extract element
    ArrayDestructuringRest,             // Collect rest elements
    ArrayDestructuringClose,            // Close iterator

    // Metadata
    SetCompletionValue                  // Set script completion to undefined
}

ExecutionPlanRunner

File: Ast/TypedAstEvaluator.ExecutionPlanRunner.cs

Core Fields

private sealed partial class ExecutionPlanRunner
{
    private readonly ExecutionPlan? _plan;
    private int _programCounter;
    private int _currentInstructionIndex;

    // Generator/async state machine
    private GeneratorState _state = GeneratorState.Start;
    private bool _done;

    // Script completion value (ES spec)
    private JsValue _scriptCompletionValue = JsValue.Unit;

    // Lazy-allocated state (only when needed)
    private TryCatchState? _tryCatchState;
    private AsyncState AsyncStateRef;
    private YieldState YieldStateRef;
    private IteratorState IteratorStateRef;
    private BreakableState BreakableStateRef;

    // Flat slots for O(1) variable access
    private JsVariable[]? _flatSlots;
}

Dispatch Table

File: Ast/TypedAstEvaluator.ExecutionPlanRunner.InstructionHandlers.cs

Handlers are stored in a delegate array indexed by InstructionKind:

private delegate InstructionResult InstructionHandler(
    ExecutionPlanRunner runner,
    ExecutionInstruction instruction,
    ref JsEnvironment environment,
    EvaluationContext context,
    out JsValue returnValue);

private static readonly InstructionHandler[] InstructionHandlers = InitializeHandlers();

private static InstructionHandler[] InitializeHandlers()
{
    var handlers = new InstructionHandler[40];
    handlers[(int)InstructionKind.Statement] = HandleStatement;
    handlers[(int)InstructionKind.Throw] = HandleThrow;
    handlers[(int)InstructionKind.BinaryOp] = HandleBinaryOp;
    handlers[(int)InstructionKind.IncrementSlot] = HandleIncrementSlot;
    handlers[(int)InstructionKind.Jump] = HandleJump;
    handlers[(int)InstructionKind.Branch] = HandleBranch;
    // ... 34 more handlers
    return handlers;
}

Execution Loop

File: Ast/TypedAstEvaluator.ExecutionPlanRunner.Loop.cs

flowchart TB
    Start((Start)) --> Fetch["Fetch instruction at PC"]
    Fetch --> CheckJump{Kind == Jump?}
    
    CheckJump -->|Yes| DoJump["PC = target"]
    DoJump --> Fetch
    
    CheckJump -->|No| CheckBranch{Kind == Branch?}
    CheckBranch -->|Yes| EvalCond["Evaluate condition"]
    EvalCond --> SetPC["PC = then/else target"]
    SetPC --> Fetch
    
    CheckBranch -->|No| Dispatch["Dispatch table lookup<br/>handlers[kind]"]
    Dispatch --> Execute["Execute handler"]
    Execute --> CheckResult{Result?}
    
    CheckResult -->|Continue| NextPC["PC = instruction.Next"]
    NextPC --> Fetch
    
    CheckResult -->|Return| Done((Return value))
    
    style CheckJump fill:#f9f,stroke:#333
    style CheckBranch fill:#f9f,stroke:#333
Loading
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private JsValue ExecuteInstructionLoop(ref JsEnvironment environment, EvaluationContext context)
{
    var instructions = _plan!.Instructions;
    var instructionsLength = instructions.Length;

    // Bounds-check-free array access
    var instructionsArray = ImmutableCollectionsMarshal.AsArray(instructions)!;
    ref var instructionsRef = ref MemoryMarshal.GetArrayDataReference(instructionsArray);

    while ((uint)_programCounter < (uint)instructionsLength)
    {
        _currentInstructionIndex = _programCounter;
        var instruction = Unsafe.Add(ref instructionsRef, _programCounter);
        var instructionKind = instruction.Kind;

        // HOT PATH: Jump (simplest instruction)
        if (instructionKind == InstructionKind.Jump)
        {
            _programCounter = Unsafe.As<JumpInstruction>(instruction).TargetIndex;
            continue;
        }

        // 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](
            this, instruction, ref environment, context, out var loopReturnValue);

        if (loopResult == InstructionResult.Return)
        {
            return loopReturnValue;
        }
    }

    _state = GeneratorState.Completed;
    _done = true;
    return CreateIteratorResult(JsValue.Undefined, true);
}

Key Handler Implementations

BinaryOp Handler

Fast-path binary operations with async signal handling:

private static InstructionResult HandleBinaryOp(
    ExecutionPlanRunner runner,
    ExecutionInstruction instr,
    ref JsEnvironment environment,
    EvaluationContext context,
    out JsValue returnValue)
{
    var instruction = Unsafe.As<BinaryOpInstruction>(instr);

    var binLeft = instruction.Left.EvaluateExpression(environment, context);
    if (context.ShouldStopEvaluation)
    {
        return HandleBinaryOpLeftSlow(...);
    }

    var binRight = instruction.Right.EvaluateExpression(environment, context);
    if (context.ShouldStopEvaluation)
    {
        return HandleBinaryOpRightSlow(...);
    }

    var binResult = ApplyBinaryOperator(instruction.Operator, binLeft, binRight, context);

    if (instruction.ResultSlot is not null)
    {
        environment.AssignJsValue(instruction.ResultSlot, binResult);
    }

    runner._programCounter = instruction.Next;
    returnValue = default;
    return InstructionResult.Continue;
}

IncrementSlot Handler (with Flat Slots)

private static InstructionResult HandleIncrementSlot(
    ExecutionPlanRunner runner,
    ExecutionInstruction instr,
    ref JsEnvironment environment,
    EvaluationContext context,
    out JsValue returnValue)
{
    var instruction = Unsafe.As<IncrementSlotInstruction>(instr);
    var flatSlotId = instruction.FlatSlotId;

    // Super-fast path: flat slot with number value
    if (flatSlotId >= 0)
    {
        ref var targetVar = ref runner._flatSlots![flatSlotId];

        if (targetVar.IsConst)
        {
            throw new ThrowSignal(StandardLibrary.CreateTypeError(
                $"Assignment to constant variable '{instruction.TargetSymbol.Name}'."));
        }

        var currentValue = targetVar.Read();
        if (currentValue.Kind == JsValueKind.Number)
        {
            var numValue = currentValue.NumberValue;
            var newValue = instruction.IsIncrement ? numValue + 1.0 : numValue - 1.0;
            targetVar.Write(newValue);
            runner._programCounter = instruction.Next;
            returnValue = default;
            return InstructionResult.Continue;
        }
    }

    return HandleIncrementSlotSlow(...);
}

Exception Handling

Files: TryEmitter.cs, ExecutionPlanRunner.cs

Try/Catch/Finally IR

flowchart TB
    subgraph Try["try block"]
        T0["0: EnterTry(catch=3, finally=6)"]
        T1["1: Statement(a())"]
        T2["2: LeaveTry(6)"]
    end
    
    subgraph Catch["catch block"]
        C3["3: EnterCatch(e)"]
        C4["4: Statement(b())"]
        C5["5: Jump(8)"]
    end
    
    subgraph Finally["finally block"]
        F6["6: StartFinally"]
        F7["7: Statement(c())"]
        F8["8: EndFinally"]
    end
    
    T0 --> T1
    T1 -->|"normal"| T2
    T1 -.->|"throw"| C3
    T2 --> F6
    C3 --> C4
    C4 --> C5
    C5 --> F8
    F6 --> F7
    F7 --> F8
    F8 --> Next((Continue))
Loading

ThrowSignal Handling

catch (ThrowSignal signal)
{
    if (context.IsThrow)
    {
        context.Clear();
    }

    if (HandleAbruptCompletion(AbruptKind.Throw, signal.ThrownValue))
    {
        // Jump to catch block, continue execution
        if (_programCounter == _currentInstructionIndex)
        {
            _programCounter = _currentInstructionIndex + 1;
        }
        continueAfterCatch = true;
        continue;
    }

    // No handler - mark complete and re-throw
    _state = GeneratorState.Completed;
    _done = true;
    throw;
}

Program Counter Semantics

Field Purpose
_programCounter Current instruction index (jump target)
_currentInstructionIndex Instruction being executed (for try/finally)

Control Flow:

  • Normal: _programCounter = instruction.Next
  • Jump: _programCounter = targetIndex
  • Branch: _programCounter = consequentIndex or alternateIndex
  • Return/Throw: Abrupt completion routing

Generator State Machine

stateDiagram-v2
    [*] --> Start : Create generator
    Start --> Executing : .next() called
    Executing --> Suspended : yield
    Suspended --> Executing : .next(value)
    Suspended --> Completed : .return(value)
    Executing --> Completed : return / end
    Suspended --> Executing : .throw(error)
    Completed --> [*]
    
    note right of Start : Never executed yet
    note right of Suspended : Paused at yield
Loading
enum GeneratorState
{
    Start,       // Never executed
    Executing,   // Currently running
    Suspended,   // Yielded, awaiting resume
    Completed    // Done
}

enum ResumeMode
{
    Next,        // iterator.next(value)
    Return,      // iterator.return(value)
    Throw        // iterator.throw(error)
}

Performance Optimizations

1. Hot Path Before Dispatch

Jump and Branch checked before dispatch table lookup.

2. Bounds-Check-Free Access

Uses Unsafe.Add and MemoryMarshal for bounds-check-free array access.

3. Flat Slots

O(1) variable access for loop counters via direct array indexing.

4. Lazy State Allocation

Try/catch, async, yield state only allocated when needed.

5. Method Inlining

Handlers marked with [MethodImpl(JsEngineConstants.Inlining)].

6. Fast/Slow Path Split

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static InstructionResult HandleFoo(...)
{
    if (fastCondition)
    {
        // ~30 lines, inlined into hot loop
        return InstructionResult.Continue;
    }
    return HandleFooSlow(...);  // NoInlining, doesn't bloat loop
}

ExecutionPlan Structure

File: Execution/ExecutionPlan.cs

internal sealed record ExecutionPlan(
    ImmutableArray<ExecutionInstruction> Instructions,
    int EntryPoint,
    int SlotCount = 0,
    ImmutableArray<Symbol> SlotSymbols = default,
    int RootSlotCount = 0,
    ImmutableDictionary<Symbol, int>? RootSlotMap = null,
    ImmutableHashSet<Symbol>? RootLexicalBindings = null,
    int RootScopeId = 0,
    int LayoutId = 0,
    int FlatSlotCount = 0,
    ImmutableDictionary<int, ImmutableArray<(int SlotIndex, int FlatSlotId)>>? FlatSlotMappings = null
);

See Also

Clone this wiki locally