-
Notifications
You must be signed in to change notification settings - Fork 1
Symbol System
The engine has two distinct symbol types that serve different purposes:
-
Symbol(compiler atoms) - Internal identifiers for the compiler/IR system -
JsSymbol(runtime) - JavaScript Symbol primitive type (ES6)
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
Location: Ast/Symbol.cs
Internal symbols used by the compiler for identifier interning and IR generation.
| Use Case | Example |
|---|---|
| Variable names | Symbol.Intern("x") |
| Built-in identifiers |
Symbol.This, Symbol.Super
|
| Synthetic temporaries | Symbol.Synthetic("__temp") |
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}");
}
}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");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);
}Location: Ast/JsSymbol.cs
The ES6 Symbol primitive type - unique, immutable values used as object property keys.
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
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
}
}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.
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);
}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;
}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");
}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]
}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
| 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 |
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
Symbolsstatic class)
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)));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");
}- JsValue System - How symbols are represented in JsValue
- JsObject & Properties - Symbol-keyed properties
-
ES Modules - How
Symbol.toStringTagaffects module namespace objects