Skip to content

CLR mapping

E# is a first-class CLR citizen: every construct lowers to ordinary metadata and IL, indistinguishable from what C# emits. This is that lowering, end to end. The IL column is what the compiler emits; the C# column is the equivalent a reader fluent in C# would recognize — a reading aid, not a step in the pipeline.

A C# project references an E# assembly with an ordinary <ProjectReference> and sees its pub surface as any C# library — no shim, adapter, or source generator:

// widget.es → Widget.dll
namespace Widgets
pub class Widget {
pub var names: List<string> { priv set }
init() { self.names = List<string>() }
pub func run(s: Stream) -> string = "ok"
}
// C# consumer, ProjectReference → Widget.esproj
var w = new Widgets.Widget();
string r = w.run(File.OpenRead(path)); // Widget's pub surface, unadorned

The emitter imports BCL types by reflection over the running runtime, where each corelib type’s assembly is the merged implementation assembly System.Private.CoreLib. A C# consumer compiles against the split reference assemblies and cannot see that implementation identity, so a reference to it is CS0012. Every BCL type reference is therefore scoped to its canonical facade at import time: the type’s assembly identity is resolved to the reference assembly that publicly exposes it as the reference is created, so every reference — a base type, a method signature, a body operand — is born consumable, with no post-emit metadata rewrite. System.Private.CoreLib, mscorlib, and netstandard are never a reference target.

Imported typeScoped to
System.Object, System.String, System.Exception, Action<T>System.Runtime
List<T>, Dictionary<K,V>System.Collections
Stream, TextReaderSystem.IO
any BCL type reflected from System.Private.CoreLib / mscorlib / netstandardits canonical System.* facade

The facade map is sourced from the running runtime’s shared-framework directory, whose facade assemblies type-forward their contract types to System.Private.CoreLib — so their forwarder tables name exactly the assembly a C# consumer references. This needs no resolved @(ReferencePath), which the .NET SDK leaves empty for a .esproj (it misclassifies the extension as a JavaScript project and skips managed reference resolution). For the same reason a consumed .esproj shall set ReferenceOutputAssembly="true", or the SDK’s IgnoreJavaScriptOutputAssembly target forces the reference to produce no assembly:

<ProjectReference Include="..\Widget\Widget.esproj" ReferenceOutputAssembly="true" />
E#C#IL
namespace Foopublic static partial class Foostatic class holding namespace methods
namespace x: T = exprstatic T xmutable static field initialized in the host .cctor
namespace let x = exprstatic T X { get; init; } (T inferred)static get_X, init-only set_X, readonly getloca_X, private backing storage
namespace var x = exprstatic T X { get; set; } (T inferred)static get_X, set_X, writable getloca_X, private backing storage
namespace let x: T => exprstatic T X => exprstatic get_X; no backing field
namespace let x: T { }static T X { get; init; }static getter/init setter/readonly location companion + private backing storage
namespace var x: T { }static T X { get; set; }static get_X / set_X + private static backing field
namespace var x: T { set(v) => expr }static property with transforming setterstatic getter/setter; setter stores expr
namespace init { body }host static constructorexact .cctor; host is not beforefieldinit
struct Point { x: int, y: int }struct Pointvalue type
pub struct Point { ... }public variant of the abovepublic variant
readonly struct R { ... }readonly struct R (+ generated equality)ValueType + [IsReadOnly]
let property: TT Property { get; init; }get_Property plus compiler-managed backing storage
expr with { f: v }IIFE copy + assignldobj + stfld (a record class: callvirt <Clone>$ + stfld)
class Config { ... }sealed partial class Configsealed class
open class Fooclass Foo (not sealed)inheritable class
abstract class Fooabstract class Fooabstract class
union Error { ... }tag enum + partial struct with factoriesValueType + tag enum
ref union Expr { ... }abstract base + sealed subclass per caseabstract class + sealed subclasses
enum Direction { ... }public enum DirectionSystem.Enum (int32)
enum Codec: byte { ... }public enum Codec : byteSystem.Enum with a byte value__ + byte case constants
interface IDrawable { ... }public interface IDrawableinterface
delegate func BinOp(...)delegate int BinOp(int, int)sealed MulticastDelegate subclass
static Foo { ... }public static partial class Foo (sibling to the namespace class)static class: const/static fields + static methods; [Extension] when it hosts one
T? (value type)Nullable<T>System.Nullable<T>
T? (ref type)T? annotationsame reference type
nil (value context)default(Nullable<T>)initobj Nullable<T>
[Serializable][Serializable]CLR custom attribute
E#C#IL
func add(a: int, b: int) -> intpublic static int add(int a, int b)call (direct)
func describe(p: Point) -> stringinstance method on Pointinstance call
func move(p: *Point, dx: int)static void move(ref Point p, int dx)managed pointer &
func f() -> T = exprT f() { return expr; }expression-body desugar
func f<T: unmanaged>(...)void f<T>(...) where T : unmanagedgeneric param with NotNullableValueType + ValueType modreq(UnmanagedType)
virtual func name(...)public virtual T name(...)Virtual + NewSlot
abstract func name(...)public abstract T name(...)Virtual + NewSlot + Abstract, no body
: func name(...) (override of virtual)public override T name(...)Virtual (ReuseSlot) — same vtable slot
: func name(...) (fulfill of abstract)public override T name(...)Virtual (ReuseSlot)
: func name(...) over an EXTERNAL base’s virtualpublic override T name(...)Virtual (ReuseSlot); accessibility taken from the base slot
static X { func (ext s: T) f() }static class X { … this T s … }[Extension] on assembly + class + method; receiver is parameter 0
static X { readonly func (ext s: T) f() }… this in T s …[Extension], in receiver
static X { func (ext s: *T) f() }… this ref T s …[Extension], ref receiver
typeof(T)typeof(T)ldtoken T + call Type::GetTypeFromHandle
&arr[i]ref arr[i]ldelema
[assembly: A(x)][assembly: A(x)]assembly-level CustomAttribute row, written once per compilation
init(args) { body } on classparameterized constructor.ctor with args
init(args) : base(b): base(b) chain.ctor with explicit call BaseType::.ctor
class E : Exceptionclass E : Exceptionderived class with a BCL extends; base .ctor chained
ex.Message (inherited BCL member)ex.Messagecallvirt Exception::get_Message, base-chain resolved
init(...) on structES3012: use composite literal / factory
returns T clausedefault return type for nested funcs missing -> T

A type that conforms to an interface must satisfy every member that interface requires, not only the members it declares. An interface’s own member list stops at its own declarations: IAsyncEnumerator<T> lists MoveNextAsync and Current, and not the DisposeAsync it requires through IAsyncDisposable. E# walks the transitive closure of the extends chain and wires each requirement to the member that satisfies it. Two rules govern the result, and a C# consumer can observe both.

A requirement is named on the interface that declares it. The extends clause is the only thing that says how a derived interface binds a base interface’s type parameters. IList<T> : ICollection<T> passes its own T through. IAsyncEnumerator<T> : IAsyncDisposable passes nothing. So the override row for DisposeAsync names IAsyncDisposable. A row naming IAsyncEnumerator<T> would reference a method that does not exist — metadata that loads, and then fails at the first call with MissingMethodException.

The emitted interface list says exactly what the type implements. The CLR rejects an override row whose declaring interface the type does not list, so every interface reached through the closure appears in the list. Nothing beyond them appears: an interface declared but not implemented loads, and then fails at the first dispatch through the empty slot.

The pub / priv / bare prefix lowers to a CLR accessibility per the position it occupies. The bare (no-prefix) form is internal, not public — and a nested type’s bare default is private, the C# nested-type default:

E#C#IL accessibility
pub x: int (field)public int xFieldAttributes.Public
x: int (bare field)internal int xFieldAttributes.Assembly
priv let x: int (property)private int X { get; init; }private accessor methods; backing field private
pub required let id: Guidpublic required Guid Id { get; init; }public accessors + [RequiredMember]
pub var x: int { } (property)public int X { get; set; }public get_X / set_X; backing field private
var x: int (unprefixed property)internal int X { get; set; }assembly get_X / set_X; backing field private
priv var x: int { } (property)private int X { get; set; }private get_X / set_X; backing field private
pub var x: int { priv set } (property)public int X { get; private set; }public get_X + private set_X; backing field private
pub struct Point / pub func fpublictop-level TypeAttributes.Public / MethodAttributes.Public
struct Point / func f (bare)internalTypeAttributes.NotPublic / MethodAttributes.Assembly
pub class Inner (nested)public nestedTypeAttributes.NestedPublic
class Inner (bare nested)internal nestedTypeAttributes.NestedAssembly
priv class Inner / bare-default nestedprivate nestedTypeAttributes.NestedPrivate

A field’s visibility lands on the field directly; a property’s lands on its get_ / set_ accessor methods, while the <name>k__BackingField stays private regardless of the prefix. The surface a C# consumer sees across the assembly boundary is exactly the pub members. The full rule is in Declarations → field & property visibility and Lexical → visibility.

A property emits as get_<name> / set_<name> accessor methods (specialname, hidebysig) bound to a PropertyDefinition, plus a private <name>k__BackingField when the property is stored — byte-identical to what C# emits, so a C# consumer sees an ordinary property.

E#C#IL
let area: T => exprT Area => expr; (computed)get_Area only, no backing field
let cur: T => self.inner.Currentforwarding computed getterget_Cur only; body loads the forwarded member
let x: T { get => expr }authored getterget_X only, no backing field
var x: T { get => read() set(v) => write(v) }behavioral propertyauthored get_X / set_X, no backing field
let x: T { } (on class)T X { get; }get_X + <X>k__BackingField (initonly); set in .ctor
required let x: T { }required T X { get; init; }get_X + set_X with modreq(IsExternalInit) + [RequiredMember]
var x: T { }T X { get; set; }get_X / set_X + <X>k__BackingField
var x: T { set(v) => body }T X { get; set { … } }get_X (auto) + custom set_X (body is the stored value)
var x: T { loca => &self.storage }property plus stable ref-location protocolordinary property + compiler-owned getloca_X companion
var x: T { mut => &self.storage }property plus direct mutation protocolordinary property + durable location companion
let x: T { mut { ... yield &working ... } }scoped read/modify protocolproperty + opaque lease + __mut_begin_X / __mut_resume_X; consumer call enclosed in finally
durable &object.property captured or live across awaitreceiver plus property access protocolordinary receiver field in the display/state machine; no managed-byref field and no *Class signature
let x: T { get } (interface)T X { get; }abstract get_X + PropertyDefinition, no field
var x: T { get set } (interface)T X { get; set; }abstract get_X / set_X + PropertyDefinition
var x: T { get set loca } (interface)property plus durable-location contractabstract get_X / set_X / ref-returning getloca_X + capability metadata
external CLR ref T X / ref readonly T Xdurable external property locationcall the actual ref-returning getter; writable or readonly direction preserved

A stored let x: T { } on a value struct is ES2193 — a struct has no init to write through it. The setter binds its value explicitly (set(v) => …); there is no contextual value name. The full surface is in Declarations → Properties.

Every emitted E# property also carries compiler-owned capability metadata. Referenced E# compilations decode that metadata directly from the PE so durable loca, direct mut, scoped mut, mutability, and custom-setter policy survive separate compilation. Generated location accessors and lease carriers are ABI companions, not source-visible *Class types. A class-valued property may use such a carrier without making *Class, &classLocal, or &classField legal. A captured or async-spilled carrier preserves the evaluated receiver and the durable location companion; it does not degrade into ordinary getter/setter access or re-enter a scoped mut protocol. Its generated storage is the receiver type, never T&.

A nested enum / struct / class / union / ref union / interface / delegate func declared in a class or static body emits as a CLR nested type, reachable as Outer.Inner and by typeof(Outer.Inner). Its accessibility uses the nested visibility flags, and the bare default is private (the C# nested-type default): a nested type without a prefix is reachable only from the enclosing type, pub makes it NestedPublic, and a bare-internal one is NestedAssembly.

E#C#IL
enum Inner { … } in a type bodynested enum Innernested System.Enum (Outer/Inner)
struct Inner { … } in a type bodynested struct Innernested value type (Outer/Inner)
class Inner { … } in a type bodynested class Innernested sealed class (Outer/Inner)
union Inner { … } / ref union Inner { … }nested union formnested tag-enum+struct / abstract base + subclasses
interface IInner { … } in a type bodynested interface IInnernested interface (Outer/IInner)
delegate func Inner(…) in a type bodynested delegate typenested sealed MulticastDelegate (Outer/Inner)
pub / bare / priv on a nested typepublic / internal / private nestedNestedPublic / NestedAssembly / NestedPrivate
E#C#IL
let x = 42var x = 42; (immutable by E#)stloc
var x = 0var x = 0;stloc
for value in arraycached-length indexed loopldlen + direct ldelem; no enumerator/finally
for i in start..endcached-end counted loopinteger compare/branch; endpoints evaluated once
a ? b : ca ? b : cconditional branch
x ?? yx ?? ynull check + branch
x?.Memberx?.Membernull-conditional access
match x { .a { } }switch (x.Tag)IL switch (jump table)
match x { .a(v1, v2) { } }multi-field destructurefield loads after tag check
match x { .a(v) { } } (ref union)is type patternisinst + field extraction
let x = f()?unwrap with early error returnconditional branch
let x = f() else { return }null guard, early returnbrfalse
defer { f() }try { ... } finally { f(); }exception handler block
"hello {name}"$"hello {name}"String.Concat (value types boxed)
E#C#IL
&funcNamedelegate*<...>ldftn + calli
&varNameref varNameldloca / ldarga / ldflda
&object.locationPropertyborrow through the property’s declared protocolgetloca_ call or scoped lease/finally protocol; receiver evaluated once
var p = &xref var p = ref xByReferenceType local
&(int, int -> int)delegate*<int, int, int>FunctionPointerType
x: *T paramref Tmanaged pointer
readonly *T paramin TByReferenceType + [In]
out x: T paramout T[Out] T&
Span<T> / ReadOnlySpan<T> indexCLR ref-returning indexerindexer call + ldobj/stobj; no boxing
xs (a T[]) into a ReadOnlySpan<T>/Span<T> slot(ReadOnlySpan<T>)xscall op_Implicit(T[]) on the span
s (a Span<T>) into a ReadOnlySpan<T> slot(ReadOnlySpan<T>)scall Span<T>::op_Implicit(Span<T>)
xs[a..b] (array)RuntimeHelpers.GetSubArray<T>(xs, a..b)System.Range + GetSubArray<T>
s[a..b] (span)s.Slice(start, length)absolute Slice(int, int) against s.Length
stackalloc byte[](n)stackalloc byte[n] (→ Span<byte>)sizeof · nlocallocnew Span<T>(void*, int)
0xEDB88320 · 0b10100xEDB88320 · 0b1010ldc.i4 / ldc.i8 (contextual width)
b"MFL1""MFL1"u8.ToArray()byte[] newarr + stelem.i1 constant fill
[1, 2, 3]new List<int> { 1, 2, 3 }List<T> + Add
(a, b)new ValueTuple<T1, T2>(a, b)System.ValueTuple
(x: T, y: U) named tuple(T x, U y)System.ValueTuple; .x.Item1; names ride [TupleElementNames]
(q: a, r: b) named construction(q: a, r: b)newobj ValueTuple<T1, T2> — labels are metadata only
let (a, b) = exprtemp + .Item1/.Item2field extraction
derive equalityEquals / GetHashCode / == / !=generated methods
static T { pub func +(a: T, b: T) -> T }public static T operator +(T, T)specialname hidebysig op_Addition
derive debugToString()generated method

Tuple element labels are erased from the CLR type — (q: int, r: int) and (int, int) are both ValueTuple<int, int> — so the names travel beside the signature slot as [System.Runtime.CompilerServices.TupleElementNames], stamped on returns, parameters, and fields. The encoding is C#‘s: one entry per tuple element across the whole type tree, depth-first, null where an element is unlabeled, so (a: int, b: (c: int, d: int)) is ["a", "b", "c", "d"] and (count: int, string) is ["count", null]. A signature carrying no labels anywhere gets no attribute. This is the only mechanism by which a C# consumer sees (int q, int r) rather than a bare ValueTuple<int, int>.

E#C#IL
await exprawait exprIAsyncStateMachine struct
spawn { ... }SpawnedOps.Spawn(() => { ... })Esharp.Stdlib.Spawned handle over Task.Run
task func name(args) -> Tstatic Spawned<T> name(args)Esharp.Stdlib.Spawned<T> joinable-future handle
chan<T>(n)new Chan<T>(n)Esharp.Stdlib.Chan<T> over Channel.CreateBounded<T>(n)
await for v in src (async stream)await foreachEsharp.Stdlib.AsyncStream channel-backed enumerator
select { ... }channel multiplexEsharp.Stdlib.ChanSelect
structured-concurrency scopescope owns childrenEsharp.Stdlib.TaskScope
List<int>()new List<int>()newobj List1::.ctor`
Dictionary<string, T>Dictionary<string, T>GenericInstanceType
Dictionary<string, byte[]>Dictionary<string, byte[]>array closes the generic arg as Byte[], not erased to Object
xs.Select(f) / span.SequenceEqual(o)same (extension methods)static call; receiver is the first argument
BCL return uint / ushort / sbyte / nint / decimalsamereflected numeric return maps to the E# primitive, not an opaque external
using static "System.Math"using static System.Math;static type import
struct Foo(x: int)positional sugarfields + positional construction

These namespaces are searched for an unqualified type name as the last resolution tier (after exact names and explicit usings — see Names & resolution), so Dictionary<…>, StringBuilder, ObservableCollection<T>, and FormatException resolve with no using:

System System.Linq
System.Collections.Generic System.Diagnostics
System.Collections.ObjectModel System.Timers
System.Collections.Specialized System.ComponentModel
System.Text System.Threading
System.Text.Json System.Threading.Tasks
System.Text.Json.Serialization System.Threading.Channels
System.IO

Disable the whole tier with <ImplicitUsings>disable</ImplicitUsings>, after which only explicitly imported namespaces resolve.