-
Notifications
You must be signed in to change notification settings - Fork 1
JsEnvironment and Slots
Roger Johansson edited this page Jan 14, 2026
·
2 revisions
JsEnvironment is the core lexical scope representation with three access patterns, from slowest to fastest.
| 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
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; }
}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
}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.
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);
}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
private sealed partial class ExecutionPlanRunner
{
// Flat slots array for O(1) variable access within this execution plan.
private JsVariable[]? _flatSlots;
}-
At lowering time:
FlatSlotMappingsmaps(scopeId, slotIndex) -> flatSlotId - PushEnvironment instruction copies current scope slots into flat array
-
Instructions have both
SlotIndexandFlatSlotId -
Fast path uses
FlatSlotIdwhen >= 0
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(...);
}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
);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;
}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);
}
}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);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);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());
}- O(1) access regardless of scope depth
- No scope chain traversal for variable reads/writes
- Super-fast arithmetic paths when both operands are in flat slots
- Memory locality - all slots in single array
- JsValue System - Value representation stored in slots
- IR Execution - How instructions use flat slots
- Performance Patterns - Environment pooling