-
Notifications
You must be signed in to change notification settings - Fork 1
Scope Analysis
How variable scoping, hoisting, and the Temporal Dead Zone (TDZ) work in Asynkron.JsEngine.
flowchart TB
subgraph Parse["Parsing"]
Source["JS Source"]
AST["AST"]
end
subgraph Analysis["Scope Analysis"]
HoistPlan["HoistPlan"]
SlotMap["SlotMap"]
ScopeId["ScopeId Assignment"]
end
subgraph Runtime["Runtime"]
Env["JsEnvironment"]
Slots["Slot Storage"]
TDZ["TDZ Markers"]
end
Source --> AST
AST --> HoistPlan
AST --> SlotMap
AST --> ScopeId
HoistPlan --> Env
SlotMap --> Slots
ScopeId --> Env
| Declaration | Hoisting | Scope | TDZ |
|---|---|---|---|
var |
Yes (initialized to undefined) | Function | No |
let |
Yes (uninitialized) | Block | Yes |
const |
Yes (uninitialized) | Block | Yes |
function |
Yes (fully hoisted) | Block* | No |
class |
Yes (uninitialized) | Block | Yes |
*Function declarations are block-scoped in strict mode.
File: Ast/HoistPlan.cs
Cached analysis of what needs to be hoisted in a block:
internal sealed class HoistPlan
{
// All let/const/class names in this block and nested blocks
internal HashSet<Symbol> LexicalNames { get; }
// Tracks if each name is const (true) or let (false)
internal Dictionary<Symbol, bool> LexicalDeclarationKinds { get; }
// Catch parameter names (create their own scope)
internal HashSet<Symbol> CatchParameterNames { get; }
// Simple catch parameters (single identifier, not destructuring)
internal HashSet<Symbol> SimpleCatchParameterNames { get; }
// Direct lexical declarations (not in nested for/if/try)
internal HashSet<Symbol> TopLevelLexicalNames { get; }
// Whether block contains function declarations
internal bool HasFunctionDeclarations { get; }
// Pre-computed templates for fast environment initialization
internal ImmutableArray<Symbol> LexicalTemplate { get; }
internal ImmutableArray<Symbol> CatchParameterTemplate { get; }
}Determines if a block requires its own lexical environment:
internal bool NeedsEnvironment =>
HasFunctionDeclarations ||
LexicalNames.Count > 0 ||
CatchParameterNames.Count > 0 ||
SimpleCatchParameterNames.Count > 0;var declarations are hoisted to the function scope and initialized to undefined:
console.log(x); // undefined (not ReferenceError)
var x = 5;
console.log(x); // 5flowchart TB
subgraph Before["Before Execution"]
V1["var x; // hoisted, initialized to undefined"]
end
subgraph During["During Execution"]
C1["console.log(x); // undefined"]
A1["x = 5;"]
C2["console.log(x); // 5"]
end
Before --> During
let/const are hoisted but NOT initialized - accessing before declaration throws:
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;flowchart TB
subgraph TDZ["Temporal Dead Zone"]
Start["Block start"]
Error["ReferenceError if accessed"]
Decl["let x = 5; // TDZ ends"]
end
Start --> Error
Error -.->|"access x"| ReferenceError
Error --> Decl
Decl --> OK["x is accessible"]
Function declarations are fully hoisted (both name and body):
foo(); // Works! Outputs "Hello"
function foo() {
console.log("Hello");
}Maps variable names to slot indices for O(1) access:
// In BlockStatement / FunctionExpression
internal ImmutableDictionary<Symbol, int> SlotMap { get; init; }
// Example: function(a, b) { let x = 1; let y = 2; }
// SlotMap: { a: 0, b: 1, x: 2, y: 3 }Each identifier is tagged with resolution info:
public sealed record IdentifierExpression(
SourceReference? Source,
Symbol Name,
int ScopeDepth = -1, // Scopes to traverse (-1 = unresolved)
int SlotIndex = -1, // Index in scope's slot array
int ScopeId = -1, // Unique scope identifier
int FlatSlotId = -1 // Index in flat slots array (IR only)
);flowchart TB
subgraph Chain["Scope Chain"]
Global((Global))
Func((Function))
Block((Block))
Inner((Inner Block))
Inner --> Block
Block --> Func
Func --> Global
end
Lookup["Lookup 'x'"]
Lookup --> Inner
Inner -.->|"not found"| Block
Block -.->|"not found"| Func
Func -.->|"found"| Result["Return value"]
Variables from outer scopes are captured by reference:
function outer() {
let x = 1;
return function inner() {
return x; // Captures 'x' from outer scope
};
}The closure maintains a reference to the outer JsEnvironment.
// When entering a block with let/const:
if (instruction.LexicalBindings is { Count: > 0 })
{
foreach (var binding in instruction.LexicalBindings)
{
if (instruction.SlotMap.TryGetValue(binding, out var slotIndex))
{
// Mark slot as uninitialized (TDZ)
newEnv.SetSlotUninitialized(slotIndex);
}
}
}// In variable access:
if (slot.Flags.HasFlag(SlotFlags.Uninitialized))
{
throw StandardLibrary.ThrowReferenceError(
$"Cannot access '{name}' before initialization");
}// When declaration is executed:
internal void SetSlotDirect(int slotIndex, JsValue value)
{
ref var slot = ref _slots![slotIndex];
slot.Value = value;
slot.Flags &= ~SlotFlags.Uninitialized; // Clear TDZ flag
}Each iteration creates a new scope with fresh bindings:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Outputs: 0, 1, 2 (not 3, 3, 3)// LoopPlan tracks per-iteration bindings
internal sealed class LoopPlan
{
internal ImmutableArray<Symbol> PerIterationBindings { get; }
internal int IterationScopeId { get; }
internal ImmutableDictionary<Symbol, int> IterationSlotMap { get; }
}Catch parameters create their own scope:
try {
throw new Error("test");
} catch (e) {
// 'e' is scoped to catch block only
console.log(e.message);
}
// 'e' is not accessible hereWalks AST to collect scope information:
// Collects:
// - Variable declarations (var, let, const)
// - Function declarations
// - Class declarations
// - Catch parameters
// - For-loop bindingsAssigns slot indices after scope analysis:
// Input: IdentifierExpression("x", ScopeDepth=-1, SlotIndex=-1)
// Output: IdentifierExpression("x", ScopeDepth=0, SlotIndex=2, ScopeId=5)In strict mode, function declarations are block-scoped:
"use strict";
{
function foo() {}
}
foo(); // ReferenceError in strict modeIn strict mode, assignment to undeclared variable throws:
"use strict";
x = 5; // ReferenceError: x is not definedHoistPlan pre-computes symbol arrays for fast environment initialization:
internal ImmutableArray<Symbol> LexicalTemplate { get; }
internal ImmutableArray<Symbol> BodyLexicalTemplate { get; }After analysis, variable access uses direct slot indexing:
// Slow path: name lookup
env.GetBindingValue(symbol); // O(n) scan
// Fast path: slot access
env.GetSlotRef(slotIndex); // O(1)For IR execution, all scopes map to a single flat array:
// FlatSlotMappings: (scopeId, slotIndex) -> flatSlotId
runner._flatSlots[flatSlotId] // O(1) regardless of scope depth- JsEnvironment & Slots - Runtime scope storage
- IR Execution - Flat slot optimization
- Architecture Overview - Execution model