Skip to content

Standard Library Architecture

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

Standard Library Architecture

How JavaScript built-in objects (Array, Promise, Map, etc.) are implemented in Asynkron.JsEngine.


Overview

flowchart TB
    subgraph Attributes["Attribute-Driven"]
        JsProto["[JsPrototype]"]
        JsCtor["[JsConstructor]"]
        JsMethod["[JsHostMethod]"]
    end
    
    subgraph Generator["Source Generator"]
        Gen["PrototypeGenerator"]
    end
    
    subgraph Output["Generated Code"]
        Proto["Prototype class"]
        Ctor["Constructor class"]
        Methods["Method bindings"]
    end
    
    subgraph Runtime["Runtime"]
        Realm["RealmState"]
        Global["Global Object"]
    end
    
    Attributes --> Generator
    Generator --> Output
    Output --> Runtime
Loading

The standard library uses attribute-based code generation to minimize boilerplate while ensuring ECMAScript compliance.


Directory Structure

StdLib/
├── Array/
│   ├── ArrayConstructor.cs      # Array constructor + static methods
│   ├── ArrayPrototype.cs        # Array.prototype methods
│   └── ArrayPrototype.Methods.cs # Method implementations
├── Promise/
│   ├── PromiseConstructor.cs
│   └── PromisePrototype.cs
├── Error/
│   ├── ErrorConstructor.cs
│   ├── TypeErrorConstructor.cs
│   └── ...
├── Intl/                        # Internationalization API
├── Temporal/                    # Temporal API
└── StandardLibrary.cs           # Error helpers, initialization

Prototype Pattern

JsPrototypeAttribute

Marks a class as a JavaScript prototype object:

[JsPrototype("Array", ToStringTag = "Array", ObjectKind = PrototypeObjectKind.Array)]
[JsSymbolAlias("iterator", "values")]  // [Symbol.iterator] = values
public sealed partial class ArrayPrototype
{
    protected override void ConfigurePrototype()
    {
        Realm.ArrayPrototype ??= Prototype;
        
        Prototype.DefineProperty("length",
            new PropertyDescriptor { Value = 0d, Writable = true, Enumerable = false, Configurable = false });
    }
}

Attribute Properties:

Property Purpose
IntrinsicName Diagnostic name (e.g., "Array")
ToStringTag Value for [Symbol.toStringTag]
ObjectKind Underlying storage (Object, Array, etc.)
InstanceType Type to extract from thisValue
TryGetMethod Custom extraction method

Method Attributes

[JsHostMethod("push", Length = 1d)]
public static JsValue Push(JsValue thisValue, IReadOnlyList<JsValue> args, RealmState? realm)
{
    // Implementation
}

[JsHostMethod("map", Length = 1d)]
public static JsValue Map(JsValue thisValue, IReadOnlyList<JsValue> args, RealmState? realm)
{
    // Implementation
}

Method Signatures:

// Instance method (receives thisValue)
JsValue Method(JsValue thisValue, IReadOnlyList<JsValue> args, RealmState? realm)

// Static method (no thisValue)
JsValue Method(IReadOnlyList<JsValue> args, RealmState? realm)

Constructor Pattern

JsConstructorAttribute

Marks a class as a JavaScript constructor:

[JsConstructor("Array", PrototypeType = typeof(ArrayPrototype), Length = 1d, DisplayName = "Array")]
public sealed partial class ArrayConstructor : JsConstructor
{
    [JsConstructorMethod("isArray", Length = 1d)]
    public static JsValue IsArray(IReadOnlyList<JsValue> args, RealmState? realm)
    {
        return new JsValue(ArrayIsArray(args.GetArgument(0), realm));
    }
    
    [JsConstructorSymbolGetter("species")]
    public static JsValue GetSpecies(JsValue thisValue)
    {
        return thisValue;  // Array[Symbol.species] returns Array
    }
    
    protected override JsValue ConstructInstance(JsValue thisValue, IReadOnlyList<JsValue> args)
    {
        var array = AllocateArrayInstance(thisValue);
        InitializeArrayLength(array, args);
        return JsValue.FromJsArray(array);
    }
}

Attribute Properties:

Property Purpose
IntrinsicName Constructor name
PrototypeType Associated prototype class
Length Function.length property
DisplayName Function.name property

Generated Code

The source generator produces:

// ArrayPrototype.g.cs (generated)
public sealed partial class ArrayPrototype : JsPrototype
{
    private void RegisterMethods()
    {
        DefineMethod("push", Push, length: 1d);
        DefineMethod("pop", Pop, length: 0d);
        DefineMethod("map", Map, length: 1d);
        // ... 30+ methods
        
        DefineSymbolMethod(SymbolKeys.Iterator, Values);  // from [JsSymbolAlias]
    }
    
    internal static JsValue RequireInstance(JsValue thisValue, RealmState realm)
    {
        if (!thisValue.TryGetObject<JsArray>(out var array))
            throw StandardLibrary.ThrowTypeError("Array method called on non-array");
        return array;
    }
}

Error Hierarchy

flowchart TB
    Error((Error))
    Error --> TypeError((TypeError))
    Error --> RangeError((RangeError))
    Error --> ReferenceError((ReferenceError))
    Error --> SyntaxError((SyntaxError))
    Error --> URIError((URIError))
    Error --> EvalError((EvalError))
    Error --> AggregateError((AggregateError))
Loading

Error Creation

// In StandardLibrary.cs
internal static JsValue CreateTypeError(string message, EvaluationContext? context = null, RealmState? realm = null)
{
    realm ??= context?.RealmState;
    if (realm?.TypeErrorConstructor is not IJsCallable callable)
    {
        return CreateErrorFallback("TypeError", message, realm);
    }
    return callable.Invoke(new SingleValueArgs(new JsValue(message)), JsValue.Null);
}

internal static ThrowSignal ThrowTypeError(string message, ...)
{
    return new ThrowSignal(CreateTypeError(message, ...));
}

Usage in Built-ins

[JsHostMethod("push")]
public static JsValue Push(JsValue thisValue, IReadOnlyList<JsValue> args, RealmState? realm)
{
    if (!thisValue.TryGetObject<JsArray>(out var array))
        throw StandardLibrary.ThrowTypeError("Array.prototype.push called on non-array", realm: realm);
    
    // Implementation...
}

RealmState

Each engine instance has a RealmState that holds constructor references:

public class RealmState
{
    // Constructors
    public IJsCallable? ArrayConstructor { get; set; }
    public IJsCallable? ObjectConstructor { get; set; }
    public IJsCallable? FunctionConstructor { get; set; }
    public IJsCallable? TypeErrorConstructor { get; set; }
    public IJsCallable? PromiseConstructor { get; set; }
    // ... 30+ constructors
    
    // Prototypes
    public IJsPropertyAccessor? ArrayPrototype { get; set; }
    public IJsPropertyAccessor? ObjectPrototype { get; set; }
    public IJsPropertyAccessor? FunctionPrototype { get; set; }
    // ... 30+ prototypes
    
    // Logger
    public ILogger? Logger { get; }
    
    // Engine reference
    public JsEngine? Engine { get; }
}

Adding a New Built-in

1. Create Prototype

// StdLib/MyType/MyTypePrototype.cs
[JsPrototype("MyType", ToStringTag = "MyType", InstanceType = typeof(MyTypeInstance))]
public sealed partial class MyTypePrototype
{
    protected override void ConfigurePrototype()
    {
        Realm.MyTypePrototype ??= Prototype;
    }
    
    [JsHostMethod("doSomething", Length = 1d)]
    public static JsValue DoSomething(JsValue thisValue, IReadOnlyList<JsValue> args, RealmState? realm)
    {
        var instance = RequireInstance(thisValue, realm);
        // Implementation...
        return JsValue.Undefined;
    }
}

2. Create Constructor

// StdLib/MyType/MyTypeConstructor.cs
[JsConstructor("MyType", PrototypeType = typeof(MyTypePrototype), Length = 0d)]
public sealed partial class MyTypeConstructor : JsConstructor
{
    protected override JsValue ConstructInstance(JsValue thisValue, IReadOnlyList<JsValue> args)
    {
        var instance = new MyTypeInstance(Realm);
        instance.SetPrototype(Prototype);
        return (JsValue)instance;
    }
    
    protected override void ConfigureConstructor(HostFunction constructor)
    {
        Realm.MyTypeConstructor ??= constructor;
    }
}

3. Register in Engine

// In engine initialization
var myTypeCtor = new MyTypeConstructor(objectPrototype, realm);
myTypeCtor.Initialize();
globalObject.DefineProperty("MyType", new PropertyDescriptor { Value = myTypeCtor.Constructor });

Symbol Methods

Symbol Aliases

[JsPrototype("Array")]
[JsSymbolAlias("iterator", "values")]  // [Symbol.iterator] points to values method
public sealed partial class ArrayPrototype { }

Symbol Getters

[JsConstructorSymbolGetter("species")]
public static JsValue GetSpecies(JsValue thisValue)
{
    return thisValue;  // Return the constructor itself
}

Symbol Keys

// Common symbol keys
SymbolKeys.Iterator         // Symbol.iterator
SymbolKeys.AsyncIterator    // Symbol.asyncIterator
SymbolKeys.ToStringTag      // Symbol.toStringTag
SymbolKeys.ToPrimitive      // Symbol.toPrimitive
SymbolKeys.Species          // Symbol.species
SymbolKeys.HasInstance      // Symbol.hasInstance
SymbolKeys.Unscopables      // Symbol.unscopables

Property Descriptors

Defining Properties

// Data descriptor
Prototype.DefineProperty("length",
    new PropertyDescriptor
    {
        Value = 0d,
        Writable = true,
        Enumerable = false,
        Configurable = false
    });

// Accessor descriptor
Prototype.DefineProperty("size",
    new PropertyDescriptor
    {
        Get = sizeGetter,
        Set = null,
        Enumerable = false,
        Configurable = true
    });

Attribute Defaults

[JsHostMethod("map", Writable = true, Enumerable = false, Configurable = true)]

Default values match ECMAScript spec:

  • Writable = true
  • Enumerable = false
  • Configurable = true

See Also

Clone this wiki locally