Skip to content

Compiler

Edgar Mesquita edited this page Jan 31, 2026 · 10 revisions

The Compiler (CSharpToJs)

The compiler is the core component that enables the magic of eQuantic.UI. It transforms C# semantics into efficient and readable TypeScript code.

🛠️ Compiler Components

1. TypeScriptEmitter

The TypeScriptEmitter is the entry point for generating .ts files. It organizes imports, defines classes, and uses the CSharpToJsConverter to convert method bodies.

2. CSharpToJsConverter

A implementation based on the Strategy pattern that traverses the Roslyn syntax tree (AST).

  • Each type of C# expression or statement has a dedicated strategy (e.g., BinaryExpressionStrategy, IfStatementStrategy).

3. SourceMapGenerator

Generates standard V3 Source Maps with Base64 VLQ encoding, mapping generated JavaScript/TypeScript back to the original .cs or .eqx source lines.

4. Identifier Heuristics

The converter applies intelligent heuristics to decide how to map variable names:

  • C# properties and fields are mapped to this.propertyName in JS.
  • Local variables and parameters retain their original names.
  • System methods like Console.WriteLine are automatically mapped to console.log.

🔄 Supported Strategies

Currently, the compiler supports a wide range of C# constructs:

  • Expressions: Arithmetic, Logical, Ternary, String Interpolation, Null-coalescing (??), Conditional Access (?., ?[])
  • Control Flow: if, switch, for, foreach, while, do-while, break, continue, throw
  • Modern Patterns: Full support for Recursive, Property, Positional, Relational, and Logical patterns (C# 9.0 - 12.0)
  • Resource Management: Support for using statements and using var declarations
  • Exceptions: Full support for try-catch-finally and throw statements (Exception → Error)
  • Indexes and Ranges: Support for index-from-end operator (array[^1]array[array.length - 1])
  • String Methods: Instance methods (Split, Replace, StartsWith, EndsWith, Contains, Substring, IndexOf, LastIndexOf, PadLeft, PadRight, Trim, TrimStart, TrimEnd, ToUpper, ToLower, ToUpperInvariant, ToLowerInvariant, Insert, Remove, ToCharArray) and static methods (IsNullOrEmpty, IsNullOrWhiteSpace, Join, Concat, Compare, Equals, Format)
  • Number Methods: int.Parse, double.Parse, float.Parse, decimal.Parse, long.Parse, int.TryParse, double.TryParse
  • List Methods: Add, AddRange, Insert, InsertRange, Remove, RemoveAt, RemoveRange, RemoveAll, Clear, IndexOf, LastIndexOf, Find, FindIndex, FindLast, FindLastIndex, FindAll, Exists, TrueForAll, Sort, ForEach, GetRange, CopyTo, BinarySearch
  • Array Static Methods: Full support for Array static methods
    • Array.Sort(array)array.sort() - Sort array in place
    • Array.Sort(array, comparison)array.sort(comparison) - Sort with custom comparer
    • Array.Reverse(array)array.reverse() - Reverse array in place
    • Array.Find(array, predicate)array.find(predicate) - Find first matching element
    • Array.FindIndex(array, predicate)array.findIndex(predicate) - Find index of first match
    • Array.FindAll(array, predicate)array.filter(predicate) - Find all matching elements
    • Array.IndexOf(array, value)array.indexOf(value) - Find index of value
    • Array.LastIndexOf(array, value)array.lastIndexOf(value) - Find last index of value
    • Array.Exists(array, predicate)array.some(predicate) - Check if any element matches
    • Array.TrueForAll(array, predicate)array.every(predicate) - Check if all elements match
    • Array.Clear(array)array.splice(0) - Clear all elements
    • Array.Resize(ref array, size)array.length = size - Resize array
  • Enum Methods: Full enum operations support
    • Enum.Parse<T>(string)parseEnum(value, EnumType) (case-insensitive)
    • Enum.TryParse<T>(string, out var result)(result = parseEnum(value, EnumType), result !== undefined)
    • Enum.GetValues<T>()Object.values(EnumType) - Get all enum values
    • Enum.GetNames<T>()Object.keys(EnumType) - Get all enum member names
    • Enum.IsDefined(typeof(T), value)(EnumType[value] !== undefined) - Validate enum value
  • Dictionary Methods: Complete Dictionary/IDictionary support
    • ContainsKey(key)(key in dict) - Check if key exists
    • TryGetValue(key, out var value)(value = dict[key]) !== undefined - Safe value retrieval
    • Add(key, value)dict[key] = value - Add or update entry
    • Remove(key)delete dict[key] - Remove entry
    • Clear()Object.keys(dict).forEach(k => delete dict[k]) - Remove all entries
    • Keys (property) → Object.keys(dict) - Get all keys as array
    • Values (property) → Object.values(dict) - Get all values as array
  • LINQ: Direct conversion of LINQ methods to JS equivalents:
    • Projection: Selectmap, SelectManyflatMap
    • Filtering: Wherefilter, Distinct[...new Set()]
    • Ordering: OrderBy/OrderByDescendingsort, Reverse[...arr].reverse()
    • Partitioning: Skipslice(n), Takeslice(0, n)
    • Element: First/FirstOrDefaultfind/[0], Last/LastOrDefaultarr[arr.length-1], Single/SingleOrDefaultfind/[0]
    • Quantifiers: Anysome/length > 0, Allevery, Containsincludes
    • Aggregation: Countlength/filter().length, Sumreduce((a,b) => a+b, 0), Averagereduce()/length, MinMath.min(...), MaxMath.max(...)
    • Set Operations:
      • Concat(other)[...source, ...other] - Concatenate two sequences
      • Union(other)[...new Set([...source, ...other])] - Unique elements from both sequences
      • Intersect(other)[...new Set(source)].filter(x => other.includes(x)) - Common elements
      • Except(other)[...new Set(source)].filter(x => !other.includes(x)) - Elements in source but not in other
    • Type Filtering:
      • Cast<T>() → passthrough (JavaScript is dynamically typed)
      • OfType<T>()filter(x => typeof x === 'type') for primitives, filter(x => x instanceof Type) for objects
  • Async/Await: Mapping of Task to Promise and native await support.
  • Modern C# Operators: Support for modern C# operators and keywords
    • Null-coalescing assignment: x ??= valuex ?? (x = value) - Assign only if null/undefined
    • nameof operator: nameof(variable)'variable' - Get name as string at compile time
    • default keyword: default(int)0, default(string)null, defaultundefined - Get default value for type

📝 Conversion Example

C# Source:

private void Increment() {
    Count++;
    if (Count > 10) Console.WriteLine("Max reached");
}

TypeScript Output:

increment() {
    this.count++;
    if (this.count > 10) console.log("Max reached");
}

🎯 Advanced Features Examples

Enum Operations

C# Source:

public enum OrderStatus { Pending, Processing, Shipped, Delivered }

private void HandleStatusChange(string input)
{
    // Parse enum from string (case-insensitive)
    if (Enum.TryParse<OrderStatus>(input, out var status))
    {
        Console.WriteLine($"Status changed to: {status}");
    }

    // Get all enum values for dropdown
    var allStatuses = Enum.GetValues<OrderStatus>();
    foreach (var s in allStatuses)
    {
        Console.WriteLine($"Available status: {s}");
    }

    // Validate enum value
    if (Enum.IsDefined(typeof(OrderStatus), "Shipped"))
    {
        Console.WriteLine("Valid status");
    }
}

TypeScript Output:

handleStatusChange(input: string) {
    // Parse with TryParse
    if ((status = parseEnum(input, OrderStatus), status !== undefined)) {
        console.log(`Status changed to: ${status}`);
    }

    // Get all values
    const allStatuses = Object.values(OrderStatus);
    for (const s of allStatuses) {
        console.log(`Available status: ${s}`);
    }

    // Validate
    if ((OrderStatus['Shipped'] !== undefined)) {
        console.log('Valid status');
    }
}

Dictionary Operations

C# Source:

private Dictionary<string, int> _settings = new();

private void ManageSettings()
{
    // Add entries
    _settings.Add("timeout", 5000);
    _settings.Add("retries", 3);

    // Check existence
    if (_settings.ContainsKey("timeout"))
    {
        var timeout = _settings["timeout"];
        Console.WriteLine($"Timeout: {timeout}");
    }

    // Safe retrieval
    if (_settings.TryGetValue("maxItems", out var max))
    {
        Console.WriteLine($"Max: {max}");
    }

    // Iterate keys
    foreach (var key in _settings.Keys)
    {
        Console.WriteLine($"{key} = {_settings[key]}");
    }

    // Clear all
    _settings.Clear();
}

TypeScript Output:

private _settings: Record<string, number> = {};

manageSettings() {
    // Add entries
    this._settings['timeout'] = 5000;
    this._settings['retries'] = 3;

    // Check existence
    if (('timeout' in this._settings)) {
        const timeout = this._settings['timeout'];
        console.log(`Timeout: ${timeout}`);
    }

    // Safe retrieval
    if ((max = this._settings['maxItems']) !== undefined) {
        console.log(`Max: ${max}`);
    }

    // Iterate keys
    for (const key of Object.keys(this._settings)) {
        console.log(`${key} = ${this._settings[key]}`);
    }

    // Clear all
    Object.keys(this._settings).forEach(k => delete this._settings[k]);
}

LINQ Set Operations

C# Source:

private void ProcessCollections()
{
    var list1 = new[] { 1, 2, 3, 4 };
    var list2 = new[] { 3, 4, 5, 6 };

    // Concatenate two lists
    var combined = list1.Concat(list2);
    // Result: [1, 2, 3, 4, 3, 4, 5, 6]

    // Union - unique elements from both
    var union = list1.Union(list2);
    // Result: [1, 2, 3, 4, 5, 6]

    // Intersect - common elements
    var common = list1.Intersect(list2);
    // Result: [3, 4]

    // Except - elements in list1 but not in list2
    var difference = list1.Except(list2);
    // Result: [1, 2]

    // Complex filtering with set operations
    var activeUsers = GetActiveUsers();
    var premiumUsers = GetPremiumUsers();

    // Users that are both active AND premium
    var activePremium = activeUsers.Intersect(premiumUsers);

    // Users that are active but NOT premium
    var activeFree = activeUsers.Except(premiumUsers);
}

TypeScript Output:

processCollections() {
    const list1 = [1, 2, 3, 4];
    const list2 = [3, 4, 5, 6];

    // Concatenate
    const combined = [...list1, ...list2];

    // Union (with Set to remove duplicates)
    const union = [...new Set([...list1, ...list2])];

    // Intersect (common elements)
    const common = [...new Set(list1)].filter(x => list2.includes(x));

    // Except (difference)
    const difference = [...new Set(list1)].filter(x => !list2.includes(x));

    // Complex filtering
    const activeUsers = this.getActiveUsers();
    const premiumUsers = this.getPremiumUsers();

    const activePremium = [...new Set(activeUsers)].filter(x => premiumUsers.includes(x));
    const activeFree = [...new Set(activeUsers)].filter(x => !premiumUsers.includes(x));
}

Array Static Methods

C# Source:

private void ProcessArrayOperations()
{
    var numbers = new[] { 5, 2, 8, 1, 9 };
    var items = new[] { "apple", "banana", "cherry" };

    // Sort array in place
    Array.Sort(numbers);
    // Result: [1, 2, 5, 8, 9]

    // Sort with custom comparison
    Array.Sort(items, (a, b) => b.Length - a.Length);
    // Result: ["banana", "cherry", "apple"]

    // Reverse array
    Array.Reverse(numbers);
    // Result: [9, 8, 5, 2, 1]

    // Find operations
    var firstEven = Array.Find(numbers, n => n % 2 == 0);
    var firstEvenIndex = Array.FindIndex(numbers, n => n % 2 == 0);
    var allEvens = Array.FindAll(numbers, n => n % 2 == 0);

    // Search operations
    var index = Array.IndexOf(numbers, 5);
    var lastIndex = Array.LastIndexOf(numbers, 5);

    // Check operations
    var hasEven = Array.Exists(numbers, n => n % 2 == 0);
    var allPositive = Array.TrueForAll(numbers, n => n > 0);

    // Clear and resize
    Array.Clear(numbers);
    Array.Resize(ref items, 5);  // Expand to 5 elements
}

TypeScript Output:

processArrayOperations() {
    const numbers = [5, 2, 8, 1, 9];
    const items = ["apple", "banana", "cherry"];

    // Sort
    numbers.sort();

    // Sort with comparison
    items.sort((a, b) => b.length - a.length);

    // Reverse
    numbers.reverse();

    // Find operations
    const firstEven = numbers.find(n => n % 2 == 0);
    const firstEvenIndex = numbers.findIndex(n => n % 2 == 0);
    const allEvens = numbers.filter(n => n % 2 == 0);

    // Search operations
    const index = numbers.indexOf(5);
    const lastIndex = numbers.lastIndexOf(5);

    // Check operations
    const hasEven = numbers.some(n => n % 2 == 0);
    const allPositive = numbers.every(n => n > 0);

    // Clear and resize
    numbers.splice(0);
    items.length = 5;
}

LINQ Type Filtering (Cast & OfType)

C# Source:

private void FilterByType()
{
    // Mixed type collection
    object[] mixed = new object[] { 1, "hello", 2, "world", 3.14, true };

    // Cast<T>() - assumes all elements are of type T (passthrough in JS)
    var assumedStrings = mixed.Cast<string>();

    // OfType<T>() - filters to only elements of type T
    var onlyStrings = mixed.OfType<string>();
    // Result: ["hello", "world"]

    var onlyNumbers = mixed.OfType<int>();
    // Result: [1, 2]

    // Works with custom classes too
    var shapes = new object[] { new Circle(), new Square(), new Circle() };
    var circles = shapes.OfType<Circle>();
    // Result: [Circle, Circle]

    // Primitive type filtering
    var primitives = new object[] { 1, "text", 2.5, true, null };
    var strings = primitives.OfType<string>();  // ["text"]
    var numbers = primitives.OfType<double>();  // [1, 2.5]
    var booleans = primitives.OfType<bool>();   // [true]
}

TypeScript Output:

filterByType() {
    // Mixed type collection
    const mixed = [1, "hello", 2, "world", 3.14, true];

    // Cast - passthrough (JS is dynamically typed)
    const assumedStrings = mixed;

    // OfType - filter by typeof for primitives
    const onlyStrings = mixed.filter(x => typeof x === 'string');
    // Result: ["hello", "world"]

    const onlyNumbers = mixed.filter(x => typeof x === 'number');
    // Result: [1, 2, 3.14]

    // OfType - filter by instanceof for objects
    const shapes = [new Circle(), new Square(), new Circle()];
    const circles = shapes.filter(x => x instanceof Circle);
    // Result: [Circle, Circle]

    // Primitive filtering
    const primitives = [1, "text", 2.5, true, null];
    const strings = primitives.filter(x => typeof x === 'string');  // ["text"]
    const numbers = primitives.filter(x => typeof x === 'number');  // [1, 2.5]
    const booleans = primitives.filter(x => typeof x === 'boolean'); // [true]
}

Modern C# Operators

C# Source:

private void DemonstrateModernOperators()
{
    // Null-coalescing assignment (??=)
    string? cachedData = null;
    cachedData ??= LoadDataFromDatabase();  // Only loads if null
    cachedData ??= "Default";               // Won't execute, already assigned

    // Property null-coalescing assignment
    if (user.Settings ??= new Settings())
    {
        Console.WriteLine("Created new settings");
    }

    // nameof operator (useful for property binding, validation)
    var propertyName = nameof(user.Email);
    Console.WriteLine($"Validating {propertyName}");  // "Validating Email"

    var methodName = nameof(ProcessOrder);
    LogAction(methodName);  // "ProcessOrder"

    // default keyword - type-safe default values
    int count = default(int);           // 0
    string? text = default(string);     // null
    bool flag = default(bool);          // false
    DateTime date = default(DateTime);  // 1/1/0001 12:00:00 AM

    // default literal (contextual)
    int number = default;               // 0 (inferred from type)
    ProcessData(default);               // passes default value for parameter type
}

private void ProcessData(int value = default)
{
    // value defaults to 0 if not provided
}

TypeScript Output:

demonstrateModernOperators() {
    // Null-coalescing assignment
    let cachedData = null;
    cachedData ?? (cachedData = this.loadDataFromDatabase());
    cachedData ?? (cachedData = 'Default');

    // Property assignment
    if (this.user.settings ?? (this.user.settings = new Settings())) {
        console.log('Created new settings');
    }

    // nameof operator
    const propertyName = 'Email';
    console.log(`Validating ${propertyName}`);

    const methodName = 'ProcessOrder';
    this.logAction(methodName);

    // default keyword
    let count = 0;
    let text = null;
    let flag = false;
    let date = null;

    // default literal
    let number = undefined;
    this.processData(undefined);
}

processData(value = 0) {
    // value defaults to 0
}

String Methods - Additional Examples

C# Source:

private void StringManipulation()
{
    var text = "  Hello World  ";

    // Trimming
    var trimmed = text.Trim();              // "Hello World"
    var leftTrim = text.TrimStart();        // "Hello World  "
    var rightTrim = text.TrimEnd();         // "  Hello World"

    // Case conversion
    var upper = text.ToUpper();             // "  HELLO WORLD  "
    var lower = text.ToLower();             // "  hello world  "
    var upperInv = text.ToUpperInvariant(); // "  HELLO WORLD  "
    var lowerInv = text.ToLowerInvariant(); // "  hello world  "

    // Chaining methods
    var clean = text.Trim().ToLower().Replace("world", "everyone");
    // Result: "hello everyone"
}

TypeScript Output:

stringManipulation() {
    const text = "  Hello World  ";

    // Trimming
    const trimmed = text.trim();
    const leftTrim = text.trimStart();
    const rightTrim = text.trimEnd();

    // Case conversion
    const upper = text.toUpperCase();
    const lower = text.toLowerCase();
    const upperInv = text.toUpperCase();
    const lowerInv = text.toLowerCase();

    // Chaining
    const clean = text.trim().toLowerCase().replaceAll("world", "everyone");
}

Clone this wiki locally