Skip to content

JsValue System

Roger Johansson edited this page Jan 14, 2026 · 2 revisions

JsValue System

The JsValue struct is the core value representation in Asynkron.JsEngine - a unified tagged union that avoids boxing for primitives.


Memory Layout

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 double to 8-byte boundary
  • Total size remains 24 bytes

JsValueKind Enum

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
Loading
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 Representations

Primitives

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
}

Reference Values

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()

Special Internal Values

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/const binding enforcement

Evaluator Overload Pattern

Problem

Evaluating statements/expressions converts between JsValue (unboxed) and object? (boxed), causing unnecessary boxing allocations in hot paths.

Solution: Multiple Overloads

  1. 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;
}
  1. 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);
}
  1. Shared core (avoid duplication):
private (JsValue jsResult, bool hasJsResult, object? objResult) EvaluateBlockCore(...)
{
    // Track JsValue separately from object results
}

Performance Impact

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.


Type Conversions

ToBoolean (ECMAScript Truthiness)

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
    };
}

ToNumber

Key conversions:

  • undefined -> NaN
  • null -> 0
  • true -> 1, false -> 0
  • "" -> 0, "123" -> 123
  • Symbol -> throws TypeError
  • BigInt -> throws TypeError (per spec)

ToPrimitive

Converts objects to primitive values using:

  1. Symbol.toPrimitive method
  2. valueOf / toString based 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...
}

Performance Optimizations

Static Singletons

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);

Integer Cache (100k Values)

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.

IAsJsValue Interface

Objects can cache their JsValue wrapper:

public interface IAsJsValue
{
    ref readonly JsValue AsJsValue { get; }
}

Implemented by JsObject, JsArray, JsMap, JsSet, HostFunction, etc.


Fast Type Checks

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;

Pattern Matching (TryGet)

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);

Implicit Conversions

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);

See Also

Clone this wiki locally