Elvelt

Frontends

C# Frontend

C# Frontend

The C# frontend is a BraceFrontendBase subclass split across four implementation files plus a large symbol-mapping table. It supports modern C# 12 features: init-only setters, the field keyword, records, pattern matching, nullable types, generics, LINQ, async/await, top-level statements, and the using declaration / using block.

File Lines Purpose
frontends/csharp/CSharpFrontend.h 73 Class declaration of CSharpFrontend
frontends/csharp/CSharpFrontend.cpp 2,131 All method bodies (parsing, expression body, LINQ, pattern matching, top-level statements)
frontends/csharp/Lexer.h / .cpp 18 / 343 Tokenizer
frontends/csharp/Parser.h / .cpp 90 / 690 Class/interface/record/enum/struct parsers, using directives
frontends/csharp/Sema.h / .cpp 51 / 596 Semantic analysis helpers (modifiers, expression body, member)
frontends/csharp/Converter.h / .cpp 31 / 369 Source normaliser (line continuation handling)
frontends/csharp/CSharpStdLib.h 43 API declaration for mapStandardLibraryType / mapStandardLibraryMethod
frontends/csharp/CSharpStdLib.cpp 1,085 Two large tables: typeMap (~570) + methodMap (~470)

Language registration

// CSharpFrontend.cpp:224-228
std::string CSharpFrontend::languageName() const { return "csharp"; }
std::vector<std::string> CSharpFrontend::fileExtensions() const { return {".cs"}; }

Only .cs is recognised.


Top-level statements wrapping

C# 9+ allows top-level executable statements outside any class or method. wrapTopLevelStatementsInProgramClass (CSharpFrontend.cpp:2104) detects top-level statements via hasTopLevelExecutableStatements (line 1922) and wraps them in a synthetic Program class with a Main entry point. If a Main method already exists or a class contains a static Main, the wrap is skipped.

using directives are also lifted from the source and emitted as ImportNodes on the module (lines 242-273):

C# IR
using System; ImportNode(module="System", metadata["csharp_using"]="true")
using static System.Math; ImportNode(metadata["static_using"]="true")
using System.Linq; ImportNode(metadata["csharp_using"]="true")
global using System; ImportNode(metadata["global_using"]="true")
using Alias = System.Foo; ImportNode(metadata["alias"]="Alias", metadata["is_alias"]="true")

// CSharpFrontend.cpp:318-335
return {
    {"Console.WriteLine", true, true},
    {"System.Console.WriteLine", true, true},
    {"Console.Write", true, false},
    {"System.Console.Write", true, false},
    {"Console.ReadLine", false, false},
    {"System.Console.ReadLine", false, false},
    {"Console.ReadKey", false, false},
    {"System.Console.ReadKey", false, false},
    {"Console.Clear", false, false},
    {"System.Console.Clear", false},
    {"System.Diagnostics.Debug.WriteLine", true},
    {"System.Diagnostics.Debug.Write", true},
    {"System.Diagnostics.Trace.WriteLine", true},
    {"System.Diagnostics.Trace.Write", true},
};

Features (file:line)

Init-only setter (init)

public class User {
    public string Name { get; init; }
}

tryParseClassFieldDecl (line 376) calls csharp::Parser::parseAutoPropertyDeclaration, which recognises init; and emits:

VariableDeclNode(name="Name", class_field="true",
  metadata={ csharp_property="auto", init_setter="true" })

field keyword (C# 13 preview)

public int Value {
    get => field;
    set => field = value;
}

Detected by tryParseClassFieldDecl (line 376) via csharp::Sema::hasExpressionBody and rawLine.find("=>"). A backing field __backing_field_Value is auto-generated, with metadata["csharp_property"] = "field_keyword".

Records

rewriteJavaRecordsToClasses doesn't apply — C# uses its own record mechanism. csharp::Parser::parseRecordDeclaration recognises:

public record Person(string Name, int Age);
public record Person {
    public string Name { get; init; }
    public int Age { get; init; }
}

Records lower to ClassDeclNode with metadata["csharp_record"]="true" and an auto-generated primary constructor.

Pattern matching

Multiple forms supported:

Form Where
x is Type (type pattern) tryParseLanguageSpecificStatement (line 1303) → __mpl_pattern_match__(x, Type)
x is Type y (declaration pattern) Same, with declaration name extracted
x is > 0 and < 100 (relational, .NET 6+) Converted to &&
x is "a" or "b" (or-pattern) Converted to ||
switch (x) { case 1: … case 2: … } SwitchNode (from BraceFrontendBase)
switch (x) { case Type y: … } Switch with type pattern → calls __mpl_pattern_match__
case var y when cond: Pattern + when clause

Nullable types

int? / string? are type-erased (the ? is dropped). The frontend doesn't currently emit Nullable<T> wrappers — see limitations.md.

Generics

Dictionary<string, int>, List<T>, IEnumerable<T>, etc. are passed through to the C++ backend. Generic type arguments are stripped from the head of method signatures via csharp::Parser::stripGenericTypeArguments (CSharpFrontend.cpp:1767).

class Foo<T> where T : class { … } is parsed but the where clause is dropped.

LINQ

isLinqQueryLine (line 786) detects LINQ query expressions:

var q = from c in customers
        where c.City == "London"
        orderby c.Name
        select c.Name;

parseLinqQuery (line 790) lowers this to a chain of FunctionCallNode invocations:

__linq_where__(customers, lambda c -> c.City == "London")
  → __linq_orderby__(..., lambda c -> c.Name)
    → __linq_select__(..., lambda c -> c.Name)

The C++ backend (backends/cpp/) defines these helpers.

LINQ method syntax

The standard extension methods are mapped via CSharpStdLib.cpp: Where, Select, SelectMany, OrderBy, ThenBy, GroupBy, Join, GroupJoin, Any, All, First, FirstOrDefault, Single, SingleOrDefault, Last, LastOrDefault, Count, LongCount, Sum, Min, Max, Average, Aggregate, Distinct, DistinctBy, Except, ExceptBy, Intersect, Union, Concat, Zip, Skip, Take, SkipWhile, TakeWhile, Reverse, Cast, OfType, Chunk.

Async / await

async modifier detected at line 1719. await expr; (line 1138) emits an AwaitNode with metadata["async"]="true".

lock, unsafe, checked, unchecked

Block Where IR
lock (expr) { … } line 1045 FunctionCallNode("__mpl_lock__", [expr, block])
unsafe { … } line 1071 BlockNode(metadata["unsafe"]="true")
checked { … } line 1086 BlockNode(metadata["checked"]="true")
unchecked { … } line 1101 BlockNode(metadata["unchecked"]="true")

yield return / yield break

Lines 1116-1135. yield return x;YieldNode(expression=parseExpr(x), metadata["generator"]="true"). yield break;YieldNode(metadata["generator"]="true").

delegate, event, fixed, using (var x), goto

Form Where IR
delegate ReturnType Name(params); line 1151 FunctionDeclNode(metadata["delegate"]="true")
event Type Name; line 1195 VariableDeclNode(metadata["event"]="true")
fixed (Type* p = &expr) { … } line 1215 FunctionCallNode("__mpl_fixed__", [decl, block])
using (var x = expr) { … } line 1240 FunctionCallNode("__mpl_using_block__", [decl, block])
goto label; line 1265 FunctionCallNode("__mpl_goto__", [label])
typeof(T), sizeof(T), nameof(e) line 1279 __mpl_typeof__, __mpl_sizeof__, __mpl_nameof__
when (pattern when clause) line 1317 FunctionCallNode("__mpl_when__", [condition])

Null-safe assignment ?.=

obj?.Prop = value; (C# 14 preview) is rewritten to __MPL_NULL_SAFE_ASSIGN__(obj, Prop, value) before parsing (line 971) and then lowered to:

IfNode(condition=`obj != null`,
       thenBranch=`obj.Prop = value`)

with metadata["csharp_null_conditional_assign"]="true".

Expression-bodied methods & properties

Type Name(params) => expr; (line 1653) emits a FunctionDeclNode with metadata["expression_body"]="expr" and metadata["expression_body_method"]="true".

Tuple deconstruction

var (a, b) = (1, 2);

isTupleDeconstruction (line 804) detects the pattern. Lowered to a VariableDeclNode for the synthesized tuple plus destructuring assignments.

Collection expressions

int[] arr = [1, 2, 3, 4];
List<int> list = [1, 2, 3];

parseCollectionExpr (line 843) lowers to __mpl_array_literal__(...) or __mpl_list_literal__(...).


Attribute usage

tryParseAttributeUsage (line 947) recognises single-line [Obsolete("msg")], [Serializable], etc. (multi-line attributes are skipped via isIgnoredLine, line 365). Attributes on the next declaration are stored as metadata["attributes"] (comma-separated).


Standard library mapping (CSharpStdLib.cpp)

The mapping is split into two tables:

typeMap (lines 10-585, ~570 entries)

Maps short C# type names to fully-qualified .NET names. Examples:

C# short .NET FQN
ASCIIEncoding System.Text.ASCIIEncoding
Action System.Action
Array System.Array
BackgroundWorker System.ComponentModel.BackgroundWorker
BigInteger System.Numerics.BigInteger
Boolean System.Boolean
Byte System.Byte
Console System.Console
Convert System.Convert
Dictionary System.Collections.Generic.Dictionary
Directory System.IO.Directory
Encoding System.Text.Encoding
Exception System.Exception
File System.IO.File
Func System.Func
Guid System.Guid
HashSet System.Collections.Generic.HashSet
HttpClient System.Net.Http.HttpClient
IEnumerable System.Collections.Generic.IEnumerable
IList System.Collections.Generic.IList
ImmutableArray System.Collections.Immutable.ImmutableArray
Int32 System.Int32
JsonSerializer System.Text.Json.JsonSerializer
Lazy System.Lazy
LinkedList System.Collections.Generic.LinkedList
List System.Collections.Generic.List
MemoryStream System.IO.MemoryStream
Nullable System.Nullable
Object System.Object
ObservableCollection System.Collections.ObjectModel.ObservableCollection
Path System.IO.Path
Queue System.Collections.Generic.Queue
Random System.Random
Regex System.Text.RegularExpressions.Regex
SemaphoreSlim System.Threading.SemaphoreSlim
Span System.Span
Stack System.Collections.Generic.Stack
Stream System.IO.Stream
StreamReader System.IO.StreamReader
StreamWriter System.IO.StreamWriter
String System.String
StringBuilder System.Text.StringBuilder
Task System.Threading.Tasks.Task
Thread System.Threading.Thread
ThreadPool System.Threading.ThreadPool
Timer System.Threading.Timer
Tuple System.Tuple
Type System.Type
Uri System.Uri
ValueTuple System.ValueTuple
XmlReader System.Xml.XmlReader
XmlWriter System.Xml.XmlWriter

(The full list is in CSharpStdLib.cpp:10-585.)

methodMap (lines 591-1078, ~470 entries)

Maps short method names to fully-qualified names. Examples:

C# short .NET FQN
Add System.Collections.Generic.List.Add
AddRange System.Collections.Generic.List.AddRange
Aggregate System.Linq.Enumerable.Aggregate
All System.Linq.Enumerable.All
Any System.Linq.Enumerable.Any
Append System.Text.StringBuilder.Append
AppendLine System.Text.StringBuilder.AppendLine
AsReadOnly System.Collections.Generic.List.AsReadOnly
Average System.Linq.Enumerable.Average
Chunk System.Linq.Enumerable.Chunk
Clear System.Text.StringBuilder.Clear
Close System.IO.Stream.Close
Concat System.Linq.Enumerable.Concat
Contains System.Linq.Enumerable.Contains
ContainsKey System.Collections.Generic.Dictionary.ContainsKey
CopyTo System.IO.Stream.CopyTo
Count System.Linq.Enumerable.Count
DefaultIfEmpty System.Linq.Enumerable.DefaultIfEmpty
Delay System.Threading.Tasks.Task.Delay
Delete System.IO.File.Delete
Deserialize System.Text.Json.JsonSerializer.Deserialize
Distinct System.Linq.Enumerable.Distinct
DistinctBy System.Linq.Enumerable.DistinctBy
ElementAt System.Linq.Enumerable.ElementAt
ElementAtOrDefault System.Linq.Enumerable.ElementAtOrDefault
Enqueue System.Collections.Generic.Queue.Enqueue
EnsureCapacity System.Collections.Generic.Dictionary.EnsureCapacity
Except System.Linq.Enumerable.Except
ExceptBy System.Linq.Enumerable.ExceptBy
Exists System.IO.File.Exists
Find System.Collections.Generic.List.Find
First System.Linq.Enumerable.First
FirstOrDefault System.Linq.Enumerable.FirstOrDefault
Flush System.IO.Stream.Flush
ForEach System.Collections.Generic.List.ForEach
GetAwaiter System.Threading.Tasks.Task.GetAwaiter
GetBytes System.Text.Encoding.GetBytes
GetCurrentDirectory System.IO.Directory.GetCurrentDirectory
GetDirectoryName System.IO.Path.GetDirectoryName
GetEncoding System.Text.Encoding.GetEncoding
GetEnumerator System.Collections.Generic.Dictionary.GetEnumerator
GetExtension System.IO.Path.GetExtension
GetFileName System.IO.Path.GetFileName
GetFullPath System.IO.Path.GetFullPath
GetString System.Text.Encoding.GetString
GetTempPath System.IO.Path.GetTempPath
GroupBy System.Linq.Enumerable.GroupBy
GroupJoin System.Linq.Enumerable.GroupJoin
Insert System.Text.StringBuilder.Insert
Intersect System.Linq.Enumerable.Intersect
Join System.Linq.Enumerable.Join
Last System.Linq.Enumerable.Last
Length System.Array.Length
LongCount System.Linq.Enumerable.LongCount
Max System.Linq.Enumerable.Max
Min System.Linq.Enumerable.Min
Next System.Collections.Generic.IEnumerator.Next
OfType System.Linq.Enumerable.OfType
OrderBy System.Linq.Enumerable.OrderBy
OrderByDescending System.Linq.Enumerable.OrderByDescending
Parse System.DateTime.Parse
Peek System.Collections.Generic.Stack.Peek
Pop System.Collections.Generic.Stack.Pop
Push System.Collections.Generic.Stack.Push
Range System.Linq.Enumerable.Range
Remove System.Collections.Generic.List.Remove
RemoveAll System.Collections.Generic.List.RemoveAll
Repeat System.Linq.Enumerable.Repeat
Replace System.Text.StringBuilder.Replace
Reverse System.Linq.Enumerable.Reverse
Select System.Linq.Enumerable.Select
SelectMany System.Linq.Enumerable.SelectMany
Serialize System.Text.Json.JsonSerializer.Serialize
Single System.Linq.Enumerable.Single
SingleOrDefault System.Linq.Enumerable.SingleOrDefault
Skip System.Linq.Enumerable.Skip
SkipWhile System.Linq.Enumerable.SkipWhile
Sleep System.Threading.Thread.Sleep
Sort System.Collections.Generic.List.Sort
Split System.String.Split
StartsWith System.String.StartsWith
Substring System.String.Substring
Sum System.Linq.Enumerable.Sum
Take System.Linq.Enumerable.Take
TakeWhile System.Linq.Enumerable.TakeWhile
ThenBy System.Linq.Enumerable.ThenBy
ThenByDescending System.Linq.Enumerable.ThenByDescending
ToArray System.Linq.Enumerable.ToArray
ToDictionary System.Linq.Enumerable.ToDictionary
ToHashSet System.Linq.Enumerable.ToHashSet
ToList System.Linq.Enumerable.ToList
ToLower System.String.ToLower
ToUpper System.String.ToUpper
Trim System.String.Trim
TryParse System.Int32.TryParse
Union System.Linq.Enumerable.Union
Wait System.Threading.Tasks.Task.Wait
WhenAll System.Threading.Tasks.Task.WhenAll
WhenAny System.Threading.Tasks.Task.WhenAny
Where System.Linq.Enumerable.Where
Write System.Console.Write
WriteLine System.Console.WriteLine
Zip System.Linq.Enumerable.Zip

IR examples

Example 1 — class with init setter

public class User {
    public string Name { get; init; }
    public int Age { get; init; }
    public User(string name, int age) {
        Name = name;
        Age = age;
    }
}

Lowered to:

ClassDeclNode(name="User",
  metadata={ access="public" })
  members:
    VariableDeclNode(name="Name", class_field="true",
      metadata={ csharp_property="auto", init_setter="true" })
    VariableDeclNode(name="Age", class_field="true",
      metadata={ csharp_property="auto", init_setter="true" })
  constructor:
    FunctionDeclNode(name="User", isConstructor=true,
      parameters=[("auto","name"), ("auto","age")])

Example 2 — LINQ query

var names = from c in customers
            where c.City == "London"
            orderby c.Name
            select c.Name;

Lowered to:

VariableDeclNode(name="names",
  initializer=FunctionCallNode("__linq_select__",
    FunctionCallNode("__linq_orderby__",
      FunctionCallNode("__linq_where__",
        IdentifierNode("customers"),
        LambdaNode(parameters=[("auto","c")],
                   expressionBody=BinaryOpNode("==", c.City, "London"))),
      LambdaNode(parameters=[("auto","c")],
                 expressionBody=MemberAccessNode(c, "Name"))),
    LambdaNode(parameters=[("auto","c")],
               expressionBody=MemberAccessNode(c, "Name"))))

Example 3 — async with pattern matching

public static double Area(Shape shape) => shape switch {
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    _ => 0.0
};

The expression-bodied switch lowers to a FunctionDeclNode with metadata["expression_body_method"]="true" and metadata["expression_body"]="…" containing the switch expression. The C++ backend expands the switch into nested IfNodes when needed.


What is not supported (C#-specific gaps)

  • Pointer types (int*, void*) — passed through but no codegen
  • fixed buffers — emitted as __mpl_fixed__ but not lowered
  • volatile, readonly fields — modifiers recognised, ignored
  • extern alias declarations
  • Operator overloads — detected (operator) but no lowering yet
  • Conversion operators (implicit/explicit) — detected, no lowering
  • Indexer detection (this[...]) — flagged in metadata but no lowering
  • record primary constructor parameter binding — class is created, but parameters are not auto-promoted to properties
  • Nullable annotations (int?) — ? is dropped, no Nullable<T> emission
  • Source generators / partial methods
  • nameof constant evaluation — emitted as a runtime call