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") |
Print patterns
// 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 fixedbuffers — emitted as__mpl_fixed__but not loweredvolatile,readonlyfields — modifiers recognised, ignoredexternalias 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 recordprimary constructor parameter binding — class is created, but parameters are not auto-promoted to properties- Nullable annotations (
int?) —?is dropped, noNullable<T>emission - Source generators /
partialmethods nameofconstant evaluation — emitted as a runtime call