-
Notifications
You must be signed in to change notification settings - Fork 1
Compiler
The compiler is the core component that enables the magic of eQuantic.UI. It transforms C# semantics into efficient and readable TypeScript code.
The TypeScriptEmitter is the entry point for generating .ts files. It organizes imports, defines classes, and uses the CSharpToJsConverter to convert method bodies.
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).
Generates standard V3 Source Maps with Base64 VLQ encoding, mapping generated JavaScript/TypeScript back to the original .cs or .eqx source lines.
The converter applies intelligent heuristics to decide how to map variable names:
- C# properties and fields are mapped to
this.propertyNamein JS. - Local variables and parameters retain their original names.
- System methods like
Console.WriteLineare automatically mapped toconsole.log.
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
usingstatements andusing vardeclarations -
Exceptions: Full support for
try-catch-finallyandthrowstatements (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:
Select→map,SelectMany→flatMap -
Filtering:
Where→filter,Distinct→[...new Set()] -
Ordering:
OrderBy/OrderByDescending→sort,Reverse→[...arr].reverse() -
Partitioning:
Skip→slice(n),Take→slice(0, n) -
Element:
First/FirstOrDefault→find/[0],Last/LastOrDefault→arr[arr.length-1],Single/SingleOrDefault→find/[0] -
Quantifiers:
Any→some/length > 0,All→every,Contains→includes -
Aggregation:
Count→length/filter().length,Sum→reduce((a,b) => a+b, 0),Average→reduce()/length,Min→Math.min(...),Max→Math.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
-
-
Projection:
-
Async/Await: Mapping of
TasktoPromiseand nativeawaitsupport. -
Modern C# Operators: Support for modern C# operators and keywords
-
Null-coalescing assignment:
x ??= value→x ?? (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,default→undefined- Get default value for type
-
Null-coalescing assignment:
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");
}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');
}
}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]);
}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));
}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;
}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]
}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
}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");
}