Skip to content

JsEnvironment and Slots

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

JsEnvironment and Slots

JsEnvironment is the core lexical scope representation with three access patterns, from slowest to fastest.


Overview

Level Mechanism Time Complexity Use Case
Named Walk scope chain, linear scan by Symbol O(scopes * slots) eval, with, unoptimized
Slot Direct index into scope's slot array O(1) within known scope Known scope, JsVariable
Flat Slot Single flat array across all scopes O(1) regardless of depth IR hot paths
flowchart TB
    subgraph Named["Level 1: Named Access (Slowest)"]
        N1((Global)) --> N2((Outer))
        N2 --> N3((Inner))
        N3 -.->|"lookup 'x'"| N2
        N2 -.->|"not found"| N1
    end
    
    subgraph Slot["Level 2: Slot Access"]
        S1["env.slots[0]"] 
        S2["env.slots[1]"]
        S3["env.slots[2]"]
    end
    
    subgraph Flat["Level 3: Flat Slot (Fastest)"]
        F["flatSlots[flatId]"]
    end
    
    Named -->|"O(n)"| Result((Value))
    Slot -->|"O(1)"| Result
    Flat -->|"O(1)"| Result
Loading

JsEnvironment Structure

File: JsEnvironment.cs

public sealed class JsEnvironment : IRentable
{
    /// Slot-based storage for variable access.
    /// Uses linear scan for name lookup - fast for typical JS scopes (1-10 bindings).
    internal JsSlot[]? _slots;

    /// Number of slots currently in use.
    internal int _slotCount;

    /// Parent scope (lexical enclosing environment).
    public JsEnvironment? Enclosing { get; set; }

    /// Tracks the expected slot layout identity for pooled environments.
    internal int LayoutId { get; private set; } = -1;

    /// Unique scope identifier for flat slot mapping.
    public int ScopeId { get; internal set; }
}

JsSlot Structure

File: JsSlot.cs

[StructLayout(LayoutKind.Sequential)]
public struct JsSlot
{
    /// The symbol name for this binding.
    public Symbol? Name;

    /// The current value of this binding.
    public JsValue Value;

    /// Packed binding flags (const, lexical, uninitialized, etc.).
    public SlotFlags Flags;
}

[Flags]
public enum SlotFlags : byte
{
    None = 0,
    Const = 1,              // const binding
    Lexical = 2,            // let/const (vs var)
    Uninitialized = 4,      // TDZ - Temporal Dead Zone
    GlobalConstant = 8,     // Built-in like undefined, NaN
    CanDelete = 32,         // Can be deleted from scope
    ImmutableBinding = 64,  // Import binding
    HasSpecialBinding = 128 // Wrapper for imports/exports
}

Level 1: Named Access (Slowest)

Lookup by Symbol through scope chain:

internal JsValue GetBindingValueDirect(Symbol name)
{
    ref var slot = ref TryGetSlotRef(name);  // Linear search
    if (!Unsafe.IsNullRef(ref slot))
    {
        if (slot.HasSpecialBinding)
        {
            return ((ISpecialBinding)slot.Value.ObjectValue!).GetJsValue();
        }
        return slot.Value;
    }
    return JsValue.Undefined;
}

internal bool TryLocateBindingInternal(Symbol name, out JsEnvironment? bindingEnvironment)
{
    var current = this;
    while (current is not null)
    {
        if (current.HasBindingLocal(name))
        {
            bindingEnvironment = current;
            return true;
        }
        current = current.Enclosing;
    }
    bindingEnvironment = null;
    return false;
}

Access Pattern: Symbol reference equality with linear O(n) scan through slots array.


Level 2: Slot Access (Medium)

Direct index into scope's slot array when scope is known:

[MethodImpl(JsEngineConstants.Inlining)]
internal ref JsSlot GetSlotByIndex(int index)
{
    return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_slots!), index);
}

[MethodImpl(JsEngineConstants.Inlining)]
internal ref JsValue GetSlotRef(int slotIndex)
{
    return ref _slots![slotIndex].Value;
}

[MethodImpl(JsEngineConstants.Inlining)]
internal void SetSlotDirect(int slotIndex, JsValue value)
{
    ref var slot = ref _slots![slotIndex];
    slot.Value = value;
    slot.Flags &= ~SlotFlags.Uninitialized;  // Clear TDZ
}

Used by JsVariable:

internal readonly struct JsVariable(JsEnvironment environment, int slotIndex)
{
    public readonly JsEnvironment Environment = environment;
    public readonly int SlotIndex = slotIndex;

    [MethodImpl(JsEngineConstants.Inlining)]
    public JsValue Read() => Environment.GetSlotRef(SlotIndex);

    [MethodImpl(JsEngineConstants.Inlining)]
    public void Write(JsValue value) => Environment.SetSlotDirect(SlotIndex, value);
}

Level 3: Flat Slot Access (Fastest)

IR-only optimization. All variables across all scopes are assigned to a single flat array.

flowchart LR
    subgraph Scopes["Nested Scopes"]
        direction TB
        Global["Global<br/>scopeId: 0"]
        Func["Function<br/>scopeId: 1"]
        Block["Block<br/>scopeId: 2"]
        Global --> Func
        Func --> Block
    end
    
    subgraph Mapping["Flat Slot Mapping"]
        direction TB
        M1["(0, 0) -> 0"]
        M2["(1, 0) -> 1"]
        M3["(1, 1) -> 2"]
        M4["(2, 0) -> 3"]
    end
    
    subgraph FlatArray["Flat Slots Array"]
        F0["[0] x"]
        F1["[1] a"]
        F2["[2] b"]
        F3["[3] i"]
    end
    
    Scopes --> Mapping
    Mapping --> FlatArray
Loading

Structure

private sealed partial class ExecutionPlanRunner
{
    // Flat slots array for O(1) variable access within this execution plan.
    private JsVariable[]? _flatSlots;
}

How It Works

  1. At lowering time: FlatSlotMappings maps (scopeId, slotIndex) -> flatSlotId
  2. PushEnvironment instruction copies current scope slots into flat array
  3. Instructions have both SlotIndex and FlatSlotId
  4. Fast path uses FlatSlotId when >= 0

Example: IncrementSlot Handler

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;
        }
    }

    // Delegate to slow path
    return HandleIncrementSlotSlow(...);
}

FlatSlotMappings

File: Execution/ExecutionPlan.cs

Part of the ExecutionPlan IR:

internal sealed record ExecutionPlan(
    // ... other fields ...
    int FlatSlotCount = 0,
    /// Maps scopeId to array of (slotIndex, flatSlotId) for eager initialization.
    ImmutableDictionary<int, ImmutableArray<(int SlotIndex, int FlatSlotId)>>? FlatSlotMappings = null
);

Initialization

private JsEnvironment InitializeExecutionEnvironment()
{
    _executionEnvironment = CreateExecutionEnvironment();

    if (_plan is null || _plan.FlatSlotCount <= 0 || _flatSlots is not null)
    {
        return _executionEnvironment;
    }

    _flatSlots = new JsVariable[_plan.FlatSlotCount];
    PopulateFlatSlotsForScope(_plan.RootScopeId, _executionEnvironment);

    // Walk closure chain
    var closureEnv = _executionEnvironment.Enclosing;
    while (closureEnv is not null)
    {
        PopulateFlatSlotsForScope(closureEnv.ScopeId, closureEnv);
        closureEnv = closureEnv.Enclosing;
    }

    return _executionEnvironment;
}

Eager Population on Scope Entry

private void PopulateFlatSlotsForScope(int scopeId, JsEnvironment environment)
{
    if (_flatSlots is null || _plan?.FlatSlotMappings is null)
        return;

    if (!_plan.FlatSlotMappings.TryGetValue(scopeId, out var mappings))
        return;

    foreach (var (slotIndex, flatSlotId) in mappings)
    {
        _flatSlots[flatSlotId] = new JsVariable(environment, slotIndex);
    }
}

IdentifierExpression Slot Metadata

File: Ast/IdentifierExpression.cs

Each identifier is tagged with resolution information:

public sealed record IdentifierExpression(
    SourceReference? Source,
    Symbol Name,
    /// Scopes up to find this variable (0 = local). -1 = unresolved.
    int ScopeDepth = -1,
    /// Index into the scope's slots array. -1 = unresolved.
    int SlotIndex = -1,
    /// Unique ID of the scope where declared. -1 = unresolved.
    int ScopeId = -1,
    /// Index into flat slots array for O(1) access. -1 = unresolved.
    int FlatSlotId = -1
) : ExpressionNode(Source);

PushEnvironmentInstruction

Instructions carry slot metadata for initialization:

internal sealed record PushEnvironmentInstruction(
    int Next,
    ImmutableArray<Symbol> PerIterationBindings,
    int ScopeId,
    int SlotCount,
    ImmutableDictionary<Symbol, int> SlotMap,
    bool AllowPooling = false,
    ImmutableHashSet<Symbol>? LexicalBindings = null,
    /// Pre-computed mappings for eager flat slot population.
    ImmutableArray<(int SlotIndex, int FlatSlotId)> FlatSlotMappings = default,
    /// Pre-computed array for fast slot name initialization.
    ImmutableArray<(Symbol Name, int SlotIndex)> SlotNames = default
) : ExecutionInstruction(InstructionKind.PushEnvironment, Next);

Slot Analysis

File: Execution/SlotAssignmentRewriter.cs

Maps identifiers to slots during compilation:

private readonly Dictionary<(int scopeId, int slotIndex), int> _flatSlotMap = new();

public int FlatSlotCount => _flatSlotMap.Count;

public ImmutableDictionary<int, ImmutableArray<(int SlotIndex, int FlatSlotId)>> BuildFlatSlotMappings()
{
    return _flatSlotMap
        .GroupBy(kv => kv.Key.scopeId)
        .ToImmutableDictionary(
            g => g.Key,
            g => g.Select(kv => (kv.Key.slotIndex, kv.Value)).ToImmutableArray());
}

Benefits of Flat Slots

  1. O(1) access regardless of scope depth
  2. No scope chain traversal for variable reads/writes
  3. Super-fast arithmetic paths when both operands are in flat slots
  4. Memory locality - all slots in single array

See Also

Clone this wiki locally