Skip to content

Symbol System

Roger Johansson edited this page Jan 14, 2026 · 1 revision

Symbol System

The engine has two distinct symbol types that serve different purposes:

  1. Symbol (compiler atoms) - Internal identifiers for the compiler/IR system
  2. JsSymbol (runtime) - JavaScript Symbol primitive type (ES6)

Architecture Overview

flowchart TB
    subgraph Compiler["Compiler Layer"]
        Symbol((Symbol))
        SymbolIntern["Symbol.Intern()"]
        SymbolSynthetic["Symbol.Synthetic()"]
    end
    
    subgraph Runtime["Runtime Layer"]
        JsSymbol((JsSymbol))
        JsSymbolCreate["JsSymbol.Create()"]
        JsSymbolFor["JsSymbol.For()"]
    end
    
    subgraph WellKnown["Well-Known Symbols"]
        Symbols((Symbols))
        SymbolKeys((SymbolKeys))
    end
    
    Symbol --> SymbolIntern
    Symbol --> SymbolSynthetic
    JsSymbol --> JsSymbolCreate
    JsSymbol --> JsSymbolFor
    Symbols --> JsSymbol
    SymbolKeys --> Symbols
Loading

Symbol (Compiler Atoms)

Location: Ast/Symbol.cs

Internal symbols used by the compiler for identifier interning and IR generation.

Purpose

Use Case Example
Variable names Symbol.Intern("x")
Built-in identifiers Symbol.This, Symbol.Super
Synthetic temporaries Symbol.Synthetic("__temp")

Implementation

public sealed class Symbol : IEquatable<Symbol>
{
    private static readonly ConcurrentDictionary<string, Symbol> Cache = new(StringComparer.Ordinal);
    private static int NextId;
    private static int NextSyntheticId;

    private readonly int _id;

    public string Name { get; }

    public static Symbol Intern(string name)
    {
        return Cache.GetOrAdd(name, n => new Symbol(n));
    }

    public static Symbol Synthetic(string prefix)
    {
        var id = Interlocked.Increment(ref NextSyntheticId);
        return Intern($"{prefix}_{id}");
    }
}

Built-in Symbols

public static readonly Symbol Undefined = Intern("undefined");
public static readonly Symbol This = Intern("this");
public static readonly Symbol Super = Intern("super");
public static readonly Symbol NewTarget = Intern("new.target");
public static readonly Symbol ImportMeta = Intern("import.meta");
public static readonly Symbol Arguments = Intern("arguments");
public static readonly Symbol Eval = Intern("eval");

// Generator/async internal symbols
public static readonly Symbol YieldResumeContextSymbol = Intern("__yieldResume__");
public static readonly Symbol GeneratorPendingCompletionSymbol = Intern("__generatorPending__");
public static readonly Symbol GeneratorInstanceSymbol = Intern("__generatorInstance__");

// Promise identifiers
public static readonly Symbol PromiseIdentifier = Intern("Promise");
public static readonly Symbol ResolveIdentifier = Intern("__resolve");
public static readonly Symbol RejectIdentifier = Intern("__reject");

// Error types
public static readonly Symbol SyntaxErrorIdentifier = Intern("SyntaxError");
public static readonly Symbol TypeErrorIdentifier = Intern("TypeError");
public static readonly Symbol ReferenceErrorIdentifier = Intern("ReferenceError");

Equality

Symbols use reference equality - two symbols with the same name are the same object due to interning:

public bool Equals(Symbol? other)
{
    return other is not null && ReferenceEquals(this, other);
}

JsSymbol (Runtime Primitive)

Location: Ast/JsSymbol.cs

The ES6 Symbol primitive type - unique, immutable values used as object property keys.

Creation Methods

flowchart LR
    subgraph Local["Local Symbols"]
        Create["JsSymbol.Create()"]
        Unique["Always Unique"]
    end
    
    subgraph Global["Global Registry"]
        For["JsSymbol.For()"]
        KeyFor["JsSymbol.KeyFor()"]
        Shared["Same Key = Same Symbol"]
    end
    
    Create --> Unique
    For --> Shared
Loading

Implementation

public sealed class JsSymbol : IJsPropertyAccessor
{
    private static readonly ConcurrentDictionary<string, JsSymbol> GlobalRegistry = new(StringComparer.Ordinal);
    private static readonly ConcurrentDictionary<int, JsSymbol> IdRegistry = new();
    private static readonly ConcurrentDictionary<int, string> PropertyKeyCache = new();
    private static int NextId;

    private readonly int _id;
    private readonly string? _key;  // Non-null for global symbols

    public string? Description { get; }

    public static JsSymbol Create(string? description = null)
    {
        return new JsSymbol(description, null, Interlocked.Increment(ref NextId));
    }

    public static JsSymbol For(string key)
    {
        return GlobalRegistry.GetOrAdd(key, k => 
            new JsSymbol(k, k, Interlocked.Increment(ref NextId)));
    }

    public static string? KeyFor(JsSymbol symbol)
    {
        return symbol._key;  // null for non-global symbols
    }
}

Property Keys

Symbols used as property keys need string representations for internal storage:

public static string PropertyKey(JsSymbol symbol)
{
    var hash = symbol.GetHashCode();
    return PropertyKeyCache.GetOrAdd(hash, h => $"@@symbol:{h}");
}

This generates keys like @@symbol:1234 for internal dictionary storage.

Reverse Lookup

internal static bool TryGetByInternalKey(string propertyName, out JsSymbol? symbol)
{
    symbol = null;
    if (!propertyName.StartsWith("@@symbol:", StringComparison.Ordinal))
        return false;

    var span = propertyName.AsSpan(9);
    if (!int.TryParse(span, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id))
        return false;

    return IdRegistry.TryGetValue(id, out symbol);
}

Prototype Methods

JsSymbol implements IJsPropertyAccessor for method access:

public bool TryGetProperty(string name, out JsValue value)
{
    if (string.Equals(name, "toString", StringComparison.Ordinal))
    {
        value = (JsValue)new HostFunction((thisValue, _) =>
        {
            if (thisValue.TryUnwrap<JsSymbol>(out var typed))
                return new JsValue(typed.ToString());
            return new JsValue("Symbol()");
        }, isConstructor: false);
        return true;
    }

    if (string.Equals(name, "valueOf", StringComparison.Ordinal))
    {
        value = (JsValue)new HostFunction((thisValue, _) => 
            (JsValue)Unbox(thisValue), isConstructor: false);
        return true;
    }
    
    // Symbol.toStringTag
    var toStringTagKey = PropertyKey(Symbols.ToStringTag);
    if (string.Equals(name, toStringTagKey, StringComparison.Ordinal))
    {
        value = (JsValue)"Symbol";
        return true;
    }

    value = JsValue.Undefined;
    return false;
}

Well-Known Symbols

Location: Ast/Symbols.cs

ES6 defines well-known symbols for customizing object behavior:

public static class Symbols
{
    public static readonly JsSymbol Iterator = JsSymbol.For("Symbol.iterator");
    public static readonly JsSymbol AsyncIterator = JsSymbol.For("Symbol.asyncIterator");
    public static readonly JsSymbol HasInstance = JsSymbol.For("Symbol.hasInstance");
    public static readonly JsSymbol ToPrimitive = JsSymbol.For("Symbol.toPrimitive");
    public static readonly JsSymbol ToStringTag = JsSymbol.For("Symbol.toStringTag");
    public static readonly JsSymbol Species = JsSymbol.For("Symbol.species");
    public static readonly JsSymbol Match = JsSymbol.For("Symbol.match");
    public static readonly JsSymbol MatchAll = JsSymbol.For("Symbol.matchAll");
    public static readonly JsSymbol Replace = JsSymbol.For("Symbol.replace");
    public static readonly JsSymbol ReplaceAll = JsSymbol.For("Symbol.replaceAll");
    public static readonly JsSymbol Search = JsSymbol.For("Symbol.search");
    public static readonly JsSymbol Split = JsSymbol.For("Symbol.split");
    public static readonly JsSymbol IsConcatSpreadable = JsSymbol.For("Symbol.isConcatSpreadable");
    public static readonly JsSymbol Unscopables = JsSymbol.For("Symbol.unscopables");
    public static readonly JsSymbol Dispose = JsSymbol.For("Symbol.dispose");
    public static readonly JsSymbol AsyncDispose = JsSymbol.For("Symbol.asyncDispose");
}

Symbol Keys Cache

Location: Ast/SymbolKeys.cs

Pre-computed property key strings for fast lookup:

public static class SymbolKeys
{
    public static readonly string Iterator = JsSymbol.PropertyKey(Symbols.Iterator);
    public static readonly string AsyncIterator = JsSymbol.PropertyKey(Symbols.AsyncIterator);
    public static readonly string HasInstance = JsSymbol.PropertyKey(Symbols.HasInstance);
    public static readonly string ToPrimitive = JsSymbol.PropertyKey(Symbols.ToPrimitive);
    public static readonly string ToStringTag = JsSymbol.PropertyKey(Symbols.ToStringTag);
    public static readonly string Species = JsSymbol.PropertyKey(Symbols.Species);
    public static readonly string Match = JsSymbol.PropertyKey(Symbols.Match);
    // ... etc
}

This allows O(1) comparisons when checking for well-known symbol properties:

if (string.Equals(name, SymbolKeys.Iterator, StringComparison.Ordinal))
{
    // Handle [Symbol.iterator]
}

Symbol Usage Flow

flowchart TD
    subgraph Creation
        JS["Symbol('desc')"] --> Create((JsSymbol.Create))
        JSFor["Symbol.for('key')"] --> For((JsSymbol.For))
    end
    
    subgraph Storage
        Create --> Id["Unique ID"]
        For --> Registry["Global Registry"]
        Id --> IdRegistry["IdRegistry"]
        Registry --> IdRegistry
    end
    
    subgraph Property
        IdRegistry --> PropKey["PropertyKey()"]
        PropKey --> KeyStr["@@symbol:N"]
        KeyStr --> ObjProps["Object Properties"]
    end
    
    subgraph Lookup
        ObjProps --> TryGet["TryGetByInternalKey()"]
        TryGet --> OrigSym["Original JsSymbol"]
    end
Loading

Comparison: Symbol vs JsSymbol

Aspect Symbol JsSymbol
Layer Compiler Runtime
Purpose Variable names, IR atoms JS Symbol primitive
Visible to JS No Yes
Equality Reference (interned) Reference (unique ID)
Global registry All interned by name Only via Symbol.for()
Property key Not used @@symbol:N format

Memory Management

Cleanup for Tests

internal static void ClearLocalSymbols()
{
    // Collect IDs of non-global symbols (those with null key)
    var localIds = IdRegistry
        .Where(kvp => kvp.Value._key is null)
        .Select(kvp => kvp.Key)
        .ToList();

    foreach (var id in localIds)
    {
        IdRegistry.TryRemove(id, out _);
        PropertyKeyCache.TryRemove(id, out _);
    }
}

This removes local symbols while preserving:

  • Global symbols (created via Symbol.for())
  • Well-known symbols (from Symbols static class)

Thread Safety

Both symbol types use ConcurrentDictionary and Interlocked operations:

// Symbol (compiler)
private static readonly ConcurrentDictionary<string, Symbol> Cache = new();
return Cache.GetOrAdd(name, n => new Symbol(n));

// JsSymbol (runtime)
private static readonly ConcurrentDictionary<string, JsSymbol> GlobalRegistry = new();
return GlobalRegistry.GetOrAdd(key, k => new JsSymbol(k, k, Interlocked.Increment(ref NextId)));

Symbol Boxing

When symbols need to be treated as objects (e.g., for method calls):

static JsSymbol Unbox(JsValue receiver)
{
    // For primitive symbols: Kind=Symbol, ObjectValue=JsSymbol
    if (receiver.IsSymbol && receiver.TryUnwrap<JsSymbol>(out var sym))
        return sym;

    // For boxed symbols: Kind=Object, ObjectValue=JsObject with __value__
    if (receiver.TryGetObject<JsObject>(out var obj) &&
        obj.TryGetProperty("__value__", out var inner) &&
        inner.IsSymbol && inner.TryUnwrap<JsSymbol>(out var innerSym))
        return innerSym;

    throw StandardLibrary.ThrowTypeError("valueOf called on incompatible receiver");
}

See Also

Clone this wiki locally