-
Notifications
You must be signed in to change notification settings - Fork 1
JsValue System
The JsValue struct is the core value representation in Asynkron.JsEngine - a unified tagged union that avoids boxing for primitives.
Size: 24 bytes on 64-bit systems
public readonly struct JsValue : IEquatable<JsValue>
{
/// The type of this value (4 bytes: int enum)
public readonly JsValueKind Kind;
// 4 bytes padding (aligns double to 8-byte boundary)
/// Stores double value, or boolean as 0.0/1.0 (8 bytes)
public readonly double NumberValue;
/// Reference for string, BigInt, Symbol, JsObject (8 bytes)
public readonly object? ObjectValue;
}Why int for Kind instead of byte?
- Better CPU performance due to alignment and cache efficiency
- Padding is needed anyway to align the
doubleto 8-byte boundary - Total size remains 24 bytes
10 distinct JavaScript value types:
flowchart TB
subgraph Primitives["Primitives (no ObjectValue)"]
Undefined((Undefined))
Null((Null))
Boolean((Boolean))
Number((Number))
end
subgraph HeapValues["Heap Values (use ObjectValue)"]
BigInt((BigInt))
String((String))
Symbol((Symbol))
Object((Object))
end
subgraph Internal["Internal Sentinels"]
Unit((Unit))
Uninitialized((Uninitialized))
end
JsValue[/"JsValue struct<br/>24 bytes"/]
JsValue --> Primitives
JsValue --> HeapValues
JsValue --> Internal
public enum JsValueKind
{
Undefined = 0, // undefined literal
Null = 1, // null literal
Boolean = 2, // true/false (stored in NumberValue as 0.0/1.0)
Number = 3, // IEEE 754 double precision
BigInt = 4, // JsBigInt in ObjectValue
String = 5, // string or JsRopeString in ObjectValue
Symbol = 6, // Symbol in ObjectValue
Object = 7, // JsObject/JsArray/etc in ObjectValue
Unit = 8, // Internal: empty completion (no value)
Uninitialized = 9 // Internal: Temporal Dead Zone (TDZ) sentinel
}| Type | Storage | Extraction |
|---|---|---|
| Boolean |
NumberValue: 1.0 for true, 0.0 for false |
value.AsBoolean() |
| Number |
NumberValue field directly |
value.AsDouble() |
| String |
ObjectValue as string or JsRopeString
|
value.AsString() |
Negative Zero: The engine preserves -0.0 semantics (distinct from +0.0 in JavaScript):
var i = (int)value;
if ((uint)i < (uint)IntegerCache.Length && i == value && !double.IsNegative(value))
{
return IntegerCache[i]; // Use cached value
}| Type | Storage | Extraction |
|---|---|---|
| BigInt |
JsBigInt object in ObjectValue
|
value.AsBigInt() |
| Symbol |
Symbol or custom wrapper in ObjectValue
|
value.AsSymbol() |
| Object |
JsObject (or derived types) in ObjectValue
|
value.AsObject() |
Unit (Empty Completion):
public static readonly JsValue Unit = new(JsValueKind.Unit, 0.0, UnitSentinel);- Represents "statement produced no value" (distinct from undefined)
- Used to distinguish completion values in statement evaluation
Uninitialized (Temporal Dead Zone):
public static readonly JsValue Uninitialized = new(JsValueKind.Uninitialized, 0.0, null);- Represents a variable in the Temporal Dead Zone (TDZ)
- Accessing throws ReferenceError
- Used for ES6
let/constbinding enforcement
Evaluating statements/expressions converts between JsValue (unboxed) and object? (boxed), causing unnecessary boxing allocations in hot paths.
- Legacy object-returning method (backwards compatibility):
private object? EvaluateBlock(BlockStatement block, JsEnvironment env, EvaluationContext ctx)
{
var (jsResult, hasJsResult, objResult) = EvaluateBlockCore(block, env, ctx);
return hasJsResult ? jsResult.ToObject() : objResult;
}- New JsValue-returning method (hot paths):
private JsValue EvaluateBlockJsValue(BlockStatement block, JsEnvironment env, EvaluationContext ctx)
{
var (jsResult, hasJsResult, objResult) = EvaluateBlockCore(block, env, ctx);
return hasJsResult ? jsResult : JsValue.FromObjectUnsafe(objResult);
}- Shared core (avoid duplication):
private (JsValue jsResult, bool hasJsResult, object? objResult) EvaluateBlockCore(...)
{
// Track JsValue separately from object results
}For ForLoop benchmark (50k let iterations):
- Allocations: 4.99 MB -> 3.84 MB (~23% reduction)
- Execution time: ~19% faster
For var loops (100k): ~8% reduction in allocations.
Falsy values: undefined, null, false, 0, -0, NaN, ""
public static bool ToBoolean(in JsValue value)
{
return value.Kind switch
{
JsValueKind.Undefined => false,
JsValueKind.Null => false,
JsValueKind.Boolean => value.NumberValue != 0,
JsValueKind.Number => !double.IsNaN(value.NumberValue) && value.NumberValue != 0,
JsValueKind.BigInt => value.ObjectValue is JsBigInt { Value.IsZero: false },
JsValueKind.String => value.ObjectValue is string { Length: > 0 },
JsValueKind.Symbol => true,
JsValueKind.Object => value.ObjectValue is not IIsHtmlDda,
_ => true
};
}Key conversions:
-
undefined->NaN -
null->0 -
true->1,false->0 -
""->0,"123"->123 -
Symbol-> throws TypeError -
BigInt-> throws TypeError (per spec)
Converts objects to primitive values using:
-
Symbol.toPrimitivemethod -
valueOf/toStringbased on hint
public static JsValue ToPrimitive(JsValue value, ToPrimitiveHint hint, EvaluationContext? context = null)
{
switch (value.Kind)
{
case JsValueKind.Undefined:
case JsValueKind.Null:
case JsValueKind.Boolean:
case JsValueKind.Number:
case JsValueKind.String:
case JsValueKind.Symbol:
case JsValueKind.BigInt:
return value; // Already primitive
}
// Object conversion via toPrimitive/valueOf/toString...
}Pre-allocated common values:
public static readonly JsValue Undefined = new(JsValueKind.Undefined, 0.0, null);
public static readonly JsValue Null = new(JsValueKind.Null, 0.0, null);
public static readonly JsValue True = new(JsValueKind.Boolean, 1.0, null);
public static readonly JsValue False = new(JsValueKind.Boolean, 0.0, null);
public static readonly JsValue Zero = new(0.0);
public static readonly JsValue One = new(1.0);
public static readonly JsValue NaN = new(double.NaN);
public static readonly JsValue EmptyString = new(string.Empty);private static readonly JsValue[] IntegerCache = CreateIntegerCache(100000);
public static JsValue FromDouble(double value)
{
var i = (int)value;
if ((uint)i < (uint)IntegerCache.Length && i == value && !double.IsNegative(value))
{
return IntegerCache[i]; // Return cached instance
}
return new JsValue(value);
}Covers common array indices and loop counters.
Objects can cache their JsValue wrapper:
public interface IAsJsValue
{
ref readonly JsValue AsJsValue { get; }
}Implemented by JsObject, JsArray, JsMap, JsSet, HostFunction, etc.
Inlined property checks:
public bool IsUndefined => Kind == JsValueKind.Undefined;
public bool IsNull => Kind == JsValueKind.Null;
public bool IsNullish => Kind <= JsValueKind.Null; // Range check: 0 or 1
public bool IsNumber => Kind == JsValueKind.Number;
public bool IsPrimitive => Kind != JsValueKind.Object;Optimized range checks compile to single CPU comparisons:
// NumberLike: Boolean or Number (uses NumberValue field)
public bool IsNumberLike => (uint)(Kind - JsValueKind.Boolean) <= 1;
// HeapValue: uses ObjectValue field
public bool IsHeapValue => Kind >= JsValueKind.String;public bool TryGetDouble(out double value);
public bool TryGetBoolean(out bool value);
public bool TryGetString([NotNullWhen(true)] out string? value);
public bool TryGetSymbol([NotNullWhen(true)] out Symbol? value);
public bool TryGetObject([NotNullWhen(true)] out JsObject? value);
public bool TryGetObject<T>([NotNullWhen(true)] out T? value) where T : class;
public bool TryGetCallable([NotNullWhen(true)] out IJsCallable? value);For convenience:
public static implicit operator JsValue(double value) => new JsValue(value);
public static implicit operator JsValue(int value) => new JsValue((double)value);
public static implicit operator JsValue(bool value) => value ? True : False;
public static implicit operator JsValue(string value) => new JsValue(value);
public static implicit operator JsValue(JsObject value) => new JsValue(value);- JsEnvironment & Slots - Variable storage using JsValue
- Performance Patterns - Caching and pooling strategies