Skip to content

Instantly share code, notes, and snippets.

@bartdesmet
Created May 7, 2024 14:42
Show Gist options
  • Save bartdesmet/cd03b104832cf1f4616fe205cdabc737 to your computer and use it in GitHub Desktop.
Save bartdesmet/cd03b104832cf1f4616fe205cdabc737 to your computer and use it in GitHub Desktop.
C# Language Internals @ Techorama 2024
using System.Collections.Immutable;
using System.Numerics;
using System.Runtime.CompilerServices;
internal class Program
{
static void Main(string[] args) => Console.WriteLine("C# Language Internals @ Techorama 2024");
}
class CSharp7
{
class TupleTypes
{
//
// How does the metadata for the names of tuple elements get stored?
//
// [return: TupleElementNames(new string[] { "a", "b", null, "g", "c", null, "f", "d", "e" })]
static (int a, int b, (int c, (int d, int e), int f), int g) Demo() => default;
// [return: TupleElementNames(new string[] { "a", "b" })]
static List<(int a, int b)> Demo2() => default;
}
class MoreMetadata
{
//
// Storing metadata to annotate types isn't new, e.g. C# 4.0 dynamic.
//
// [return: Dynamic]
static dynamic Foo() => default;
// [return: Dynamic(new bool[] { false, false, false, true })]
static Dictionary<string, List<dynamic>> Bar() => default;
//
// And also for nullable reference types in C# 8.0.
//
// [return: Nullable(new byte[] { 1, 2 })]
static List<string?> Qux(string[] xs) => default;
}
class LocalFunctions
{
//
// This generates a static method:
//
// static int <Static>g__F|0_0()
//
static void Static()
{
int x = F();
int F() => 42;
}
//
// This generates a struct-based closure that's passed by-ref:
//
// static int <Closure>g__F|1_0(ref <>c__DisplayClass1_0 P_0)
//
static void Closure()
{
int x = 42;
int y1 = F();
int y2 = F();
int F() => x++;
}
//
// This simply constructs a delegate to the generated static method:
//
// return <StaticHoist>g__F|2_0;
//
static Func<int> StaticHoist()
{
int F() => 42;
return F;
}
//
// This generates a class-based closure with a generated instance method:
//
// <>c__DisplayClass3_0<> c__DisplayClass3_ = new <> c__DisplayClass3_0();
// <>c__DisplayClass3_.x = 42;
// return <>c__DisplayClass3_.<ClosureHoist>g__F|0;
//
static Func<int> ClosureHoist()
{
int x = 42;
int F() => x++;
return F;
}
}
class PatternMatching
{
//
// Each `is` expression expands to a set of Boolean checks,
// with possible repeated re-evaluations.
//
static string Is(object o)
{
if (o is null) // if (o == null)
{
return "null";
}
else if (o is 42) // if (o is int && (int)o == 42)
{
return "42";
}
else if (o is int x) // if (o is int)
{
return x.ToString(); // return ((int)o).ToString();
}
else
{
return "other";
}
}
//
// Here the compiler can reason across different `case`s.
//
static string Switch(object o)
{
//if (o != null)
//{
// if (o is int)
// {
// int x = (int)o; // Note there's only one `int` check and cast.
// if (x == 42)
// {
// return "42";
// }
// return x.ToString();
// }
// return "other";
//}
//return "null";
switch (o)
{
case null:
return "null";
case 42:
return "42";
case int x:
return x.ToString();
default:
return "other";
}
}
}
}
class CSharp8
{
class NullableReferenceTypes
{
//
// Metadata is used to indicate where the `?` annotations are:
//
// 1 = non-null
// 2 = nullable
//
static void F(string s) { }
static void F(string[] s) { }
// static void F2([Nullable(new byte[] { 2, 1 })] string[] s)
static void F2(string[]? s) { }
// [NullableContext(2)]
static void G(string? s) { }
// static void G([Nullable(new byte[] { 1, 2 })] string[] s)
static void G(string?[] s) { }
// [NullableContext(2)]
static void G2(string?[]? s) { }
}
class IndexAndRange
{
//
// Indexes are converted to int-based index values.
//
// xs[i.GetOffset(xs.Length)]
static int ArrayByIndex(int[] xs, Index i) => xs[i];
// xs[i.GetOffset(xs.Count)]
static int ListByIndex(List<int> xs, Index i) => xs[i];
// s[i.GetOffset(s.Length)]
static char StringByIndex(string s, Index i) => s[i];
//
// From-end expressions turn into `Index` values with `FromEnd` set.
// This gets lowered further to an int-based index value.
//
// xs[xs.Length - 1]
static int ArrayFromEnd(int[] xs) => xs[^1];
// xs[xs.Count - 1]
static int ListFromEnd(List<int> xs) => xs[^1];
// s[s.Length - 1]
static char StringFromEnd(string s) => s[^1];
//
// Ranges construct new instances using runtime helper methods,
// or using well-known methods like `Slice` and `Substring`.
//
// RuntimeHelpers.GetSubArray(xs, Range.All)
static int[] ArrayRangeAll(int[] xs) => xs[..];
// xs.Slice(0, xs.Count)
static List<int> ListRangeAll(List<int> xs) => xs[..];
// s.Substring(0, s.Length) // Note this returns `s` itself.
static string StringRangeAll(string s) => s[..];
// RuntimeHelpers.GetSubArray(xs, Range.EndAt(new Index(1, true)))
static int[] ArraySuffix(int[] xs) => xs[..^1];
// xs.Slice(0, xs.Count - 1)
static List<int> ListSuffix(List<int> xs) => xs[..^1];
// s.Substring(0, s.Length - 1)
static string StringSuffix(string s) => s[..^1];
// RuntimeHelpers.GetSubArray(xs, Range.StartAt(1))
static int[] ArrayPrefix(int[] xs) => xs[1..];
// xs.Slice(1, xs.Count - 1)
static List<int> ListPrefix(List<int> xs) => xs[1..];
// s.Substring(1, s.Length - 1)
static string StringPrefix(string s) => s[1..];
// RuntimeHelpers.GetSubArray(xs, new Range(1, new Index(1, true)))
static int[] ArraySlice(int[] xs) => xs[1..^1];
// xs.Slice(1, xs.Count - 1 - 1)
static List<int> ListSlice(List<int> xs) => xs[1..^1];
// s.Substring(1, s.Length - 1 - 1)
static string StringSlice(string s) => s[1..^1];
}
class AsyncStreams
{
static async IAsyncEnumerable<int> F(int begin, int end)
{
//
// An async iterator has two possible suspensions:
//
// 1. `await` expressions
// 2. `yield return` statements
//
// A state machine is built to rule them all. Notable differences with
// regular async methods are:
//
// 1. Use of `ManualResetValueTaskSourceCore<bool> <>v__promiseOfValueOrEnd`
// to coordinate `yield return`, `yield break` (or end), and exceptions
// between producer (`MoveNext` state machine) and consumer (`MoveNextAsync`).
// 2. Use of `bool <>w__disposeMode` to track request to `DisposeAsync` and
// run the cleanup code path (e.g. `finally` blocks) when disposing.
// 3. Use of `int <>l__initialThreadId` akin to the synchronous iterator
// pattern where an `IEnumerable<T>` morphs into an `IEnumerator<T>` when
// enumeration is triggered on the same thread. Otherwise, a clone is made.
//
for (int i = begin; i < end; i++)
{
await Task.Yield();
yield return i;
}
}
static async Task Demo()
{
//
// This desugars into:
//
// IAsyncEnumerator<int> asyncEnumerator = F(0, 10).GetAsyncEnumerator();
// try
// {
// while (await asyncEnumerator.MoveNextAsync())
// {
// Console.WriteLine(asyncEnumerator.Current);
// }
// }
// finally
// {
// if (asyncEnumerator != null)
// {
// await asyncEnumerator.DisposeAsync();
// }
// }
//
// which could be abbreviated using `await using` as well. Further inspection
// into the `await` in `finally` (added in C# 6.0) could be done:
//
// 1. `finally` is turned into `catch (object ex) { ex_ = ex; }`.
// 2. The `ex_` variable ends up in the async state machine object.
// 3. Code in `finally` is run unconditionally.
// 4. The `ex_` variable is checked for `null`, and any exception is rethrown.
//
await foreach (var x in F(0, 10))
{
Console.WriteLine(x);
}
}
}
}
class CSharp9
{
class RecordTypes
{
//
// Record types generate:
//
// 1. A class (or struct):
// `class Person`
// 2. Init-only properties:
// `string Name { get; init; }`
// 3. Implementation of `IEquatable<T>` delegating to the properties:
// a. Equals and chaining from `object.Equals`.
// b. GetHashCode using a multiple-add hash combine.
// 4. Operators `==` and `!=` chaining to `Equals`.
// 5. `Deconstruct` support:
// `void Deconstruct(out string Name, out int Age)`
// 6. `ToString` support:
// a. `ToString` override
// b. `PrintMembers(StringBuilder)` protected utility method.
// 7. A "copy constructor" pattern for use by `with`:
// a. `protected Person(Person original)`
// b. `virtual Person <Clone>$()`
// 8. Support for inheritance:
// a. The clone method is `virtual`.
// b. The `PrintMembers` functionality enables reuse by derived types.
// c. The `Equals` implementation uses an `Type EqualityContract`.
//
record Person(string Name, int Age);
//
// `with` expressions work for other types besides records. In case of
// record types, it uses the clone functionality:
//
// Person person = p.<Clone>$();
// person.Age = p.Age + 1;
//
static Person Birthday(Person p) => p with { Age = p.Age + 1 };
}
class CovariantReturnTypes
{
abstract class Base
{
public abstract object F();
}
//
// A more derived return type can be used in an override. This
// required changes to the CLR. The generated C# code is:
//
// [PreserveBaseOverrides]
// public new virtual string F()
//
// with the following IL details:
//
// .override method instance object CSharp9/CovariantReturnTypes/Base::F()
//
// The new `PreserveBaseOverrides` attribute is used to ensure that
// virtual dispatch always calls the most-derived implementation, see
// https://github.com/dotnet/runtime/blob/main/docs/design/features/covariant-return-methods.md
//
class Derived : Base
{
public override string F() => default;
}
}
class RelationalAndLogicalPatterns
{
//
// Relational patterns simply become range checks. Note that a decompiler
// like ILSpy may get confused:
//
// switch (c)
// {
// case 'A':
// ...
// case 'Z':
// case 'a':
// ...
// case 'z':
// return true;
// }
//
// but the IL code is simply a few `bge` and `blt` branch instructions.
//
static bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
}
}
class CSharp10
{
class InterpolatedStringHandlers
{
//
// Enable efficient string interpolation to custom logger frameworks etc. A
// few highlights on the implementation are below.
//
enum LogLevel
{
Info,
Warning,
Error,
}
//
// An instance method for logging, where interpolated strings can be assigned
// to parameter `s` of type `MyString`, which is an interpolated string handler.
//
// The use of `InterpolatedStringHandlerArgument` instructs the compiler to pass
// extra parameters to the constructor of `MyString`, in this case the `this`
// instance, and the `level` parameter, allowing the interpolated string handler
// to perform checks (e.g. don't do any formatting/allocations/etc. when the log
// level is not warranting producing output).
//
void Log(LogLevel level, [InterpolatedStringHandlerArgument("", nameof(level))] MyString s)
{
}
void Demo(int x, int y)
{
//
// This produces code to manufacture a MyString instance:
//
// MyString s = new MyString(10, 2, this, level);
// if (s.AppendLiteral("x = ") && s.AppendFormatted(x) && s.AppendLiteral(", y = "))
// {
// s.AppendFormatted(y, "D");
// }
//
// Note a few things:
// 1. Magic argument values `10` and `2` match:
// a. 10 is the literal string length, combined:
// $"x = {x}, y = {y:D}"
// 1234 56789A
// b. 2 is the number of interpolations:
// $"x = {x}, y = {y:D}"
// -1- --2--
// 2. The `this` and `level` parameters are passed to the constructor.
// 3. Calls to `Append*` methods return `bool` (this is optional) and
// results are checked. This enables an interpolated string handler
// to stop the formatting early, e.g. when exceeding a length.
//
Log(LogLevel.Info, $"x = {x}, y = {y:D}");
}
[InterpolatedStringHandler]
struct MyString
{
public MyString(int literalLength, int interpolationCount, InterpolatedStringHandlers logger, LogLevel level)
{
}
public bool AppendLiteral(string s) => true;
public bool AppendFormatted<T>(T value) => true;
public bool AppendFormatted<T>(T value, string format) => true;
}
}
class PropertyPatterns
{
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public City City { get; set; }
}
class City
{
public string Name { get; set; }
}
//
// An extended property pattern is equivalent to:
//
// p is { City: { Name: "Antwerp" } }
//
// which implies `null` checks:
//
// p != null ... city != null ...
//
static bool Demo(Person p) => p is { City.Name: "Antwerp" };
}
class AsyncMethodBuilders
{
//
// C# has supported custom async method builders since adding support
// for `ValueTask` and `ValueTask<T>`-returning `async` methods. C# 10
// added support to override the builder at the method level.
//
// We can use this as an opportunity to look behind the scenes of the
// async method builder pattern.
//
class MyTask<T>
{
}
[AsyncMethodBuilder(typeof(MyTaskBuilder<>))]
static async MyTask<int> F()
{
await Task.Yield();
return 42;
}
class MyTaskBuilder<T>
{
//
// The `F()` method gets compiled into a "ramp" function to construct
// an async state machine instance and return a task. The state machine
// object implements the logic in the body of the async method.
//
// Focusing on the ramp function first:
//
// <F>d__2 stateMachine = default(< F > d__2);
// stateMachine.<>t__builder = MyTaskBuilder<int>.Create();
// stateMachine.<>1__state = -1;
// stateMachine.<>t__builder.Start(ref stateMachine);
// return stateMachine.<>t__builder.Task;
//
// Therefore, we need to provide the following:
//
// 1. A static `Create` method.
// 2. A `Start` method to kick off the state machine.
// 3. A `Task` property to obtain the task to return.
//
public MyTask<T> Task => default;
public static MyTaskBuilder<T> Create() => default;
//
// Note the use of `ref` to avoid having to box the struct-based
// state machine object. The state machine only gets hoisted to the
// heap by `SetStateMachine` below in case we hit a suspension, i.e.
// we don't run to completion synchronously.
//
public void Start<TAsyncStateMachine>(ref TAsyncStateMachine stateMachine)
where TAsyncStateMachine : IAsyncStateMachine
{
}
//
// See above. This can get called in the async completion path by an
// awaiter, see `AwaitOnCompleted`, to hoist the state machine to the heap.
//
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
//
// The body of the `async` method gets rewritten to `MoveNext` method on
// the state machine type:
//
// struct <F>d__2 : IAsyncStateMachine
// {
// public int <>1__state;
// // More fields go here.
//
// void MoveNext()
// {
// // State machine code goes here.
// }
// }
//
// This `MoveNext` method has a macroscopic structure that looks like:
//
// try
// {
// // User code transformed into a state machine.
// }
// catch (Exception exception)
// {
// <>1__state = -2;
// <>t__builder.SetException(exception);
// }
// <>1__state = -2;
// <>t__builder.SetResult(result);
//
// where the `-2` state indicates completion, and control flow is massaged
// such that all `return` statements end up in the `SetResult` call. So, we
// need to provide:
//
// 1. SetException to capture an unhandled exception in user code
// 2. SetResult to store the result of the async method
//
public void SetException(Exception error) { }
public void SetResult(T result) { }
//
// Finally, all `await` expressions in the body of the `async` method get
// transformed into the `GetAwaiter` pattern, e.g. for:
//
// await Task.Yield()
//
// we get:
//
// YieldAwaitable.YieldAwaiter awaiter;
// if (num != 0)
// {
// awaiter = Task.Yield().GetAwaiter();
// if (!awaiter.IsCompleted)
// {
// num = (<>1__state = 0);
// <>u__1 = awaiter;
// <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
// return;
// }
// else
// {
// awaiter = <>u__1;
// <>u__1 = default(YieldAwaitable.YieldAwaiter);
// num = (<>1__state = -1);
// }
// awaiter.GetResult();
//
// Note a few things:
//
// 1. The awaiter is obtained through `GetAwaiter` and is stored in a local.
// 2. If the awaiter `IsCompleted` check returns false, the awaiter is lifted
// to a field in the state machine object.
// 3 .Upon completion, synchronous or otherwise, a call to `GetResult` is made.
//
// The interesting path is the asynchronous path, where asynchronous completion
// is mediated through a call to `Await[Unsafe]OnCompleted` on the builder,
// which gets its hand on the awaiter which implements `INotifyCompletion` and
// can decide how to trigger the resumption when it's done, e.g.:
//
// awaiter.OnCompleted(stateMachine.MoveNext)
//
public void AwaitOnCompleted<TAwaiter, TAsyncStateMachine>(ref TAwaiter awaiter, ref TAsyncStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TAsyncStateMachine : IAsyncStateMachine
{
}
public void AwaitUnsafeOnCompleted<TAwaiter, TAsyncStateMachine>(ref TAwaiter awaiter, ref TAsyncStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TAsyncStateMachine : IAsyncStateMachine
{
}
}
}
}
class CSharp11
{
class GenericMath
{
//
// Generic math is based on static abstract methods. E.g.
//
// IL_0000: ldarg.0
// IL_0001: ldarg.1
// IL_0002: constrained. !!T
// IL_0008: call !2 class [System.Runtime]System.Numerics.IAdditionOperators`3<!!T, !!T, !!T>::op_Addition(!0, !1)
//
// using `constrained` calls in generic code, which avoids boxing for value types.
//
static T MidPoint<T>(T first, T second)
where T : IAdditionOperators<T, T, T>, IDivisionOperators<T, T, T>, IMultiplicativeIdentity<T, T>
=> (first + second) / (T.MultiplicativeIdentity + T.MultiplicativeIdentity);
class MyNumber : IAdditionOperators<MyNumber, MyNumber, MyNumber>, IDivisionOperators<MyNumber, MyNumber, MyNumber>, IMultiplicativeIdentity<MyNumber, MyNumber>
{
public static MyNumber MultiplicativeIdentity => throw new NotImplementedException();
public static MyNumber operator +(MyNumber left, MyNumber right)
{
throw new NotImplementedException();
}
public static MyNumber operator /(MyNumber left, MyNumber right)
{
throw new NotImplementedException();
}
}
static int Demo(int x, int y) => MidPoint(x, y);
static MyNumber Demo(MyNumber x, MyNumber y) => MidPoint(x, y);
}
class ListPatterns
{
//
// List patterns check the expected length based on patterns in element
// positions, apply patterns to elements, and can extract `..` in a way
// similar to C# 8.0 ranges.
//
// xs != null && xs.Length == 0
static bool Empty(int[] xs) => xs is [];
// xs != null && xs.Count == 1 && xs[0] == 1
static bool Single(List<int> xs) => xs is [1];
// s != null && s.Length >= 1 && s[0] == 'a'
static bool Prefix(string s) => s is ['a', ..];
// int length = xs.Length; // Note: struct type, so no null check needed.
// return length >= 1 && xs[length - 1] == 1;
static bool Suffix(ImmutableArray<int> xs) => xs is [.., 1];
// if (xs != null)
// {
// int num = xs.Length;
// if (num >= 2 && xs[0] == 1)
// {
// return xs[num - 1] == 9;
// }
// }
static bool PrefixAndSuffix(int[] xs) => xs is [1, .., 9];
// if (xs != null)
// {
// int num = xs.Length;
// if (num >= 2 && xs[0] == 1)
// {
// // Note the left-to-right evaluation of sub-patterns.
// int[] ys = RuntimeHelpers.GetSubArray(xs, new Range(new Index(1), new Index(1, true)));
// if (xs[num - 1] == 9)
// {
// return ys.Length == 7;
// }
// }
// }
static bool PrefixAndSuffixSlice(int[] xs) => xs is [1, .. var ys, 9] && ys.Length == 7;
// Similar to the above, just using `Slice` instead.
static bool PrefixAndSuffixSlice(List<int> xs) => xs is [1, .. var ys, 9] && ys.Count == 7;
}
class RequiredMembers
{
//
// How does a `set` become `init`?
//
// .set instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit)
//
// How does a property become `required`?
//
// [RequiredMember]
//
// How does it get guaranteed?
//
// [Obsolete("Constructors of types with required members are not supported in this version of your compiler.", true)]
// [CompilerFeatureRequired("RequiredMembers")]
// public Person()
//
class Person
{
public required string Name { get; init; }
public required int Age { get; init; }
}
}
}
class CSharp12
{
class LambdaOptionalParameter
{
//
// The compiler synthesizes custom delegate types, which are generic to
// allow for reuse:
//
// delegate TResult <>f__AnonymousDelegate0<T1, TResult>(T1 arg = 1);
// delegate TResult <>f__AnonymousDelegate1<T1, TResult>(params T1[] arg);
//
// If signatures of type-inferred lambda expressions match Func<> or Action<>
// delegate types, those are used.
//
static void Demo()
{
var f = long (int x = 1) => x + 1;
var g = (params int[] xs) => xs.Length;
}
}
class CollectionExpressions
{
//
// Efficient uniform construction of collection type instances.
//
// Array.Empty<int>()
static int[] Empty() => [];
//
// Uses a `CollectionsMarshal.AsSpan(list)` mechanism to stuff values
// in the underlying array without needing repeated bounds checks:
//
// List<int> list = new List<int>(1);
// CollectionsMarshal.SetCount(list, 1);
// Span<int> span = CollectionsMarshal.AsSpan(list);
// span[0] = 1;
//
static List<int> One() => [1];
// ImmutableCollectionsMarshal.AsImmutableArray(new int[2] { 1, 2 })
static ImmutableArray<int> Many() => [1, 2];
// Uses `list.AddRange(xs)` to append `xs`.
static int[] Spread(IEnumerable<int> xs) => [1, ..xs, 2];
//
// Can compute the length of `xs`, so can optimize:
//
// int num = 0;
// int[] array = new int[2 + xs.Count];
// array[num++] = 1;
// Span<int> span = CollectionsMarshal.AsSpan(xs);
// span.CopyTo(new Span<int>(array).Slice(num2, span.Length));
// num += span.Length;
// array[num] = 2;
//
static int[] Spread(List<int> xs) => [1, ..xs, 2];
}
class PrimaryConstructors
{
//
// The parameters to the primary constructor are in scope of the class
// or struct, and can get captured in fields.
//
class Point(int x, int y)
{
//
// A read-only property generates:
//
// private readonly int <X>k__BackingField;
//
// and
//
// public int X { get { return <X>k__BackingField; } }
//
// The right-hand side of the assignment uses `x` and is lifted to
// the constructor:
//
// <X>k__BackingField = x < 0 ? throw new ArgumentOutOfRangeException(nameof(x)) : x;
//
public int X { get; } = x < 0 ? throw new ArgumentOutOfRangeException(nameof(x)) : x;
//
// A get-only property with an expression body referencing a primary
// constructor parameter causes a closure to be created:
//
// private int <y>P;
//
// and the property becomes:
//
// public int Y { get { return <y>P; } }
//
public int Y => y;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment