Skip to content

BigInt Implementation

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

BigInt Implementation

This page documents how Asynkron.JsEngine implements JavaScript BigInt - arbitrary precision integers as specified in ES2020+.

Architecture Overview

flowchart TB
    subgraph JsValue["JsValue (24 bytes)"]
        Kind["Kind = BigInt (4)"]
        NumberValue["NumberValue = 0.0"]
        ObjectValue["ObjectValue → JsBigInt"]
    end
    
    subgraph JsBigInt["JsBigInt"]
        Value["Value: BigInteger"]
        Operators["Operators: +, -, *, /, %, **, &, |, ^, ~, <<, >>"]
    end
    
    subgraph SystemNumerics["System.Numerics"]
        BigInteger["BigInteger\n(arbitrary precision)"]
    end
    
    ObjectValue --> JsBigInt
    JsBigInt --> BigInteger
Loading

Core Types

JsBigInt Wrapper

BigInt values are stored as JsBigInt objects that wrap .NET's System.Numerics.BigInteger:

File: JsTypes/JsBigInt.cs

public sealed class JsBigInt(BigInteger value) : IEquatable<JsBigInt>
{
    public BigInteger Value { get; } = value;
    
    // Pre-allocated constants to reduce allocations
    public static JsBigInt Zero => new(BigInteger.Zero);
    public static JsBigInt One => new(BigInteger.One);
}

JsValue Storage

BigInt uses the ObjectValue field in JsValue:

File: JsTypes/JsValue.cs

/// <summary>Creates a BigInt value.</summary>
[MethodImpl(JsEngineConstants.Inlining)]
public JsValue(JsBigInt value)
{
    Kind = JsValueKind.BigInt;
    NumberValue = 0.0;
    ObjectValue = value;
}

public bool IsBigInt
{
    [MethodImpl(JsEngineConstants.Inlining)]
    get => Kind == JsValueKind.BigInt;
}

[MethodImpl(JsEngineConstants.Inlining)]
public JsBigInt AsBigInt() => (JsBigInt)ObjectValue!;

Arithmetic Operations

Operator Implementations

All arithmetic operators are implemented directly on JsBigInt:

File: JsTypes/JsBigInt.cs

Operator Implementation
+ new JsBigInt(left.Value + right.Value)
- new JsBigInt(left.Value - right.Value)
* new JsBigInt(left.Value * right.Value)
/ Division with zero check, truncates towards zero
% Remainder with zero check
** BigInteger.Pow(base, (int)exponent)
- (unary) new JsBigInt(-operand.Value)
// Division with zero check
public static JsBigInt operator /(JsBigInt left, JsBigInt right)
{
    if (right.Value == BigInteger.Zero)
    {
        throw new DivideByZeroException("Division by zero");
    }
    return new JsBigInt(left.Value / right.Value);
}

// Power operation
public static JsBigInt Pow(JsBigInt baseValue, JsBigInt exponent)
{
    if (exponent.Value < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(exponent), "Exponent must be non-negative");
    }
    return new JsBigInt(BigInteger.Pow(baseValue.Value, (int)exponent.Value));
}

Runtime Operation Dispatch

Binary operations dispatch based on operand types:

File: Ast/TypedAstEvaluator.JsValue.cs

private static JsValue NumericBinaryOp(
    in JsValue left,
    in JsValue right,
    Func<double, double, double> numericOp,
    Func<JsBigInt, JsBigInt, JsBigInt> bigIntOp,
    EvaluationContext context)
{
    // Fast path: both are numbers
    if (left.IsNumber && right.IsNumber)
    {
        return JsValue.FromDouble(numericOp(left.NumberValue, right.NumberValue));
    }
    
    // Convert to numeric (BigInt or Number)
    var leftNumeric = ToNumericValue(left, context);
    var rightNumeric = ToNumericValue(right, context);
    
    // Both BigInt
    if (leftNumeric.IsBigInt && rightNumeric.IsBigInt)
    {
        return bigIntOp(leftNumeric.AsBigInt(), rightNumeric.AsBigInt());
    }
    
    // Both Number
    if (leftNumeric.IsNumber && rightNumeric.IsNumber)
    {
        return JsValue.FromDouble(numericOp(leftNumeric.NumberValue, rightNumeric.NumberValue));
    }
    
    // Mixed types - TypeError
    throw ThrowTypeError("Cannot mix BigInt and other types", context);
}

Bitwise Operations

BigInt supports all bitwise operations except unsigned right shift (>>>):

File: JsTypes/JsBigInt.cs

Operation Implementation Notes
& (AND) left.Value & right.Value Works on infinite precision
| (OR) left.Value | right.Value Works on infinite precision
^ (XOR) left.Value ^ right.Value Works on infinite precision
~ (NOT) ~operand.Value Returns -(n + 1)
<< Left shift with sign handling Negative shifts reverse direction
>> Right shift with sign handling Negative shifts reverse direction
// Left shift with negative handling
public static JsBigInt operator <<(JsBigInt left, JsBigInt right)
{
    var shiftAmount = (int)right.Value;
    return shiftAmount < 0
        ? new JsBigInt(left.Value >> -shiftAmount)
        : new JsBigInt(left.Value << shiftAmount);
}

Unsigned Right Shift Restriction

Per ECMAScript spec, >>> is not supported for BigInt:

File: Ast/TypedAstEvaluator.cs

private static JsValue UnsignedRightShiftJsValue(in JsValue left, in JsValue right, EvaluationContext context)
{
    var leftNumeric = ToNumericValue(left, context);
    
    if (leftNumeric.IsBigInt)
    {
        throw ThrowTypeError("Cannot use unsigned right shift on BigInt", context);
    }
    
    // ... Number handling
}

Type Coercion

ToBigInt Conversion

File: StdLib/StandardLibrary.cs

flowchart TD
    Input["ToBigInt(value)"]
    
    Input --> Check{value.Kind?}
    
    Check -->|undefined| TypeError1["TypeError"]
    Check -->|null| TypeError2["TypeError"]
    Check -->|boolean| Boolean["false → 0n\ntrue → 1n"]
    Check -->|bigint| Return["return value"]
    Check -->|number| NumberCheck{Is integer?}
    Check -->|string| Parse["Parse as BigInt\n(hex/bin/octal)"]
    Check -->|symbol| TypeError3["TypeError"]
    Check -->|object| ToPrimitive["ToPrimitive(Number)\nthen recurse"]
    
    NumberCheck -->|yes| Convert["Convert to BigInt"]
    NumberCheck -->|no| RangeError["RangeError"]
Loading
internal static JsBigInt ToBigInt(JsValue value, EvaluationContext? context = null)
{
    return value.Kind switch
    {
        JsValueKind.Undefined => throw ThrowTypeError("Cannot convert undefined to BigInt"),
        JsValueKind.Null => throw ThrowTypeError("Cannot convert null to BigInt"),
        JsValueKind.Boolean => value.IsTrue ? JsBigInt.One : JsBigInt.Zero,
        JsValueKind.BigInt => value.AsBigInt(),
        JsValueKind.Number => ConvertNumberToBigInt(value.NumberValue),
        JsValueKind.String => ParseBigIntString((string)value.ObjectValue!),
        JsValueKind.Symbol => throw ThrowTypeError("Cannot convert Symbol to BigInt"),
        JsValueKind.Object => ToBigInt(ToPrimitive(value, "number"), context),
        _ => throw new InvalidOperationException()
    };
}

Comparison with Numbers

BigInt can be compared with Numbers using relational operators:

File: Runtime/JsOps.cs

private static int? CompareBigIntToDouble(BigInteger bigInt, double number)
{
    // Handle special cases
    if (double.IsNaN(number)) return null;
    if (double.IsPositiveInfinity(number)) return -1;  // bigInt < +Infinity
    if (double.IsNegativeInfinity(number)) return 1;   // bigInt > -Infinity
    
    // Compare mathematical values with precision handling
    var bigIntDouble = (double)bigInt;
    if (bigIntDouble < number) return -1;
    if (bigIntDouble > number) return 1;
    
    // For exact comparison when values are close
    if (BigInteger.TryParse(number.ToString("R"), out var numberAsBigInt))
    {
        return bigInt.CompareTo(numberAsBigInt);
    }
    
    return 0;
}

Built-in Functions

BigInt Constructor

File: StdLib/BigInt/BigIntConstructor.cs

// Cannot be called with new
new BigInt(123);  // TypeError

// Must be called as function
BigInt(123);      // 123n
BigInt("456");    // 456n
BigInt(true);     // 1n

Static Methods

Method Description
BigInt.asIntN(bits, value) Wraps value to signed N-bit integer
BigInt.asUintN(bits, value) Wraps value to unsigned N-bit integer
// asIntN - signed integer truncation
[JsHostMethod("asIntN", Length = 2d)]
public JsValue AsIntN(JsValue _, IReadOnlyList<JsValue> args)
{
    var bits = (int)ToNumber(args.GetArgument(0));
    var value = ToBigInt(args.GetArgument(1));
    
    var mod = BigInteger.One << bits;
    var result = value.Value % mod;
    
    // Convert to signed if in upper half
    if (result >= (BigInteger.One << (bits - 1)))
    {
        result -= mod;
    }
    
    return new JsBigInt(result);
}

Prototype Methods

File: StdLib/BigInt/BigIntPrototype.cs

Method Description
toString(radix) Converts to string with radix 2-36
valueOf() Returns primitive BigInt value
toLocaleString() Locale-aware string conversion

Typed Arrays

BigInt has dedicated typed arrays for 64-bit integers:

Files:

  • JsTypes/JsBigInt64Array.cs
  • JsTypes/JsBigUint64Array.cs
  • JsTypes/BigIntTypedArrayBase.cs
// BigInt64Array - signed 64-bit integers
public sealed class JsBigInt64Array : BigIntTypedArrayBase<long>
{
    protected override int BytesPerElement => 8;
    
    protected override JsValue GetElement(int index)
    {
        var value = BinaryPrimitives.ReadInt64LittleEndian(
            Buffer.Data.AsSpan(ByteOffset + index * 8));
        return new JsBigInt(value);
    }
}

// BigUint64Array - unsigned 64-bit integers
public sealed class JsBigUint64Array : BigIntTypedArrayBase<ulong>
{
    // Similar implementation with ulong
}

Performance Optimizations

1. Inlining

All hot path methods use aggressive inlining:

[MethodImpl(JsEngineConstants.Inlining)]
public bool IsBigInt => Kind == JsValueKind.BigInt;

2. Fast Path Checks

Binary operations check for Number-only paths first:

// Fast path - most common case
if (left.IsNumber && right.IsNumber)
{
    return JsValue.FromDouble(left.NumberValue + right.NumberValue);
}

// Slower path - handle BigInt and mixed types

3. Static Instances

Pre-allocated constants reduce allocations:

public static JsBigInt Zero => new(BigInteger.Zero);
public static JsBigInt One => new(BigInteger.One);

4. Increment/Decrement Optimization

File: Ast/TypedAstEvaluator.ExecutionPlanRunner.Handlers.Operators.cs

else if (incCurrentValue.IsBigInt)
{
    var bigInt = (JsBigInt)incCurrentValue.ObjectValue!;
    incOldNumericValue = incCurrentValue;
    var incNewBigInt = instruction.IsIncrement
        ? bigInt.Value + 1
        : bigInt.Value - 1;
    incNewJsValue = new JsBigInt(incNewBigInt);
}

Mixed Type Behavior

flowchart LR
    subgraph Allowed["Allowed Operations"]
        A1["1n + 2n = 3n"]
        A2["1n < 2 = true"]
        A3["1n == 1 = true"]
        A4["1n === 1n = true"]
    end
    
    subgraph Forbidden["TypeError"]
        F1["1n + 2"]
        F2["1n * 2"]
        F3["Math.sqrt(4n)"]
        F4["1n >>> 0"]
    end
Loading
Operation BigInt + BigInt BigInt + Number Result
+, -, *, /, % Allowed TypeError BigInt
<, >, <=, >= Allowed Allowed Boolean
== Allowed Allowed Boolean
=== Allowed Always false Boolean
>>> TypeError N/A N/A

Key Files

Category File Path
Core Type src/Asynkron.JsEngine/JsTypes/JsBigInt.cs
Value Storage src/Asynkron.JsEngine/JsTypes/JsValue.cs
Kind Enum src/Asynkron.JsEngine/JsTypes/JsValueKind.cs
Constructor src/Asynkron.JsEngine/StdLib/BigInt/BigIntConstructor.cs
Prototype src/Asynkron.JsEngine/StdLib/BigInt/BigIntPrototype.cs
Arithmetic src/Asynkron.JsEngine/Ast/TypedAstEvaluator.JsValue.cs
Bitwise src/Asynkron.JsEngine/Ast/TypedAstEvaluator.cs
Coercion src/Asynkron.JsEngine/StdLib/StandardLibrary.cs
Comparison src/Asynkron.JsEngine/Runtime/JsOps.cs
BigInt64Array src/Asynkron.JsEngine/JsTypes/JsBigInt64Array.cs
Tests tests/Asynkron.JsEngine.Tests/BigIntTests.cs

See Also

Clone this wiki locally