Things I've learned about the .NET runtime

The JIT is actually required

(Or, in NativeAOT, other mechanisms that aren’t just what’s in System.Private.CoreLib.)

Some methods are JIT intrinsics, but I had previously thought the JIT was just an optimisation. This is false! For example, Type.TypeHandle throws and must be swapped out by the JIT. I don’t know how this works on NativeAOT.

Console.WriteLine is incredibly complicated

Console.WriteLine exercises at least the following:

Top-level no-exception-handler behaviour change in net9

In earlier versions of .NET, finally blocks and Dispose methods were liable not to run when an unhandled exception terminates program execution. This was documented as implementation-defined behaviour in the C# language spec, though it doesn’t appear to be documented elsewhere.

That implementation-defined behaviour means my programs basically all contain this boilerplate:

1
2
3
4
5
6
7
8
let reallyMain argv =
    0

let main argv =
    try
        reallyMain argv
    with
    | _ -> reraise ()

However, in .NET 9 (I think) this behaviour appears to have been changed, and now the .NET runtime does the obvious thing - Dispose methods run and finally blocks execute even when an exception causes shutdown!

The .NET runtime doesn’t actually model uint32

Neither on the eval stack nor as a CLI intrinsic type are uint32 or uint64 modelled; they’re stored as int32 and int64 with two’s complement.