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.
Reference assemblies & C# consumption
Section titled “Reference assemblies & C# consumption”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.dllnamespace Widgetspub 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.esprojvar w = new Widgets.Widget();string r = w.run(File.OpenRead(path)); // Widget's pub surface, unadornedThe 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 type | Scoped to |
|---|---|
System.Object, System.String, System.Exception, Action<T> | System.Runtime |
List<T>, Dictionary<K,V> | System.Collections |
Stream, TextReader | System.IO |
any BCL type reflected from System.Private.CoreLib / mscorlib / netstandard | its 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" />Types & members
Section titled “Types & members”| E# | C# | IL |
|---|---|---|
namespace Foo | public static partial class Foo | static class holding namespace methods |
namespace x: T = expr | static T x | mutable static field initialized in the host .cctor |
namespace let x = expr | static T X { get; init; } (T inferred) | static get_X, init-only set_X, readonly getloca_X, private backing storage |
namespace var x = expr | static T X { get; set; } (T inferred) | static get_X, set_X, writable getloca_X, private backing storage |
namespace let x: T => expr | static T X => expr | static 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 setter | static getter/setter; setter stores expr |
namespace init { body } | host static constructor | exact .cctor; host is not beforefieldinit |
struct Point { x: int, y: int } | struct Point | value type |
pub struct Point { ... } | public variant of the above | public variant |
readonly struct R { ... } | readonly struct R (+ generated equality) | ValueType + [IsReadOnly] |
let property: T | T Property { get; init; } | get_Property plus compiler-managed backing storage |
expr with { f: v } | IIFE copy + assign | ldobj + stfld (a record class: callvirt <Clone>$ + stfld) |
class Config { ... } | sealed partial class Config | sealed class |
open class Foo | class Foo (not sealed) | inheritable class |
abstract class Foo | abstract class Foo | abstract class |
union Error { ... } | tag enum + partial struct with factories | ValueType + tag enum |
ref union Expr { ... } | abstract base + sealed subclass per case | abstract class + sealed subclasses |
enum Direction { ... } | public enum Direction | System.Enum (int32) |
enum Codec: byte { ... } | public enum Codec : byte | System.Enum with a byte value__ + byte case constants |
interface IDrawable { ... } | public interface IDrawable | interface |
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? annotation | same reference type |
nil (value context) | default(Nullable<T>) | initobj Nullable<T> |
[Serializable] | [Serializable] | CLR custom attribute |
Methods, inheritance & constructors
Section titled “Methods, inheritance & constructors”| E# | C# | IL |
|---|---|---|
func add(a: int, b: int) -> int | public static int add(int a, int b) | call (direct) |
func describe(p: Point) -> string | instance method on Point | instance call |
func move(p: *Point, dx: int) | static void move(ref Point p, int dx) | managed pointer & |
func f() -> T = expr | T f() { return expr; } | expression-body desugar |
func f<T: unmanaged>(...) | void f<T>(...) where T : unmanaged | generic 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 virtual | public 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 class | parameterized constructor | .ctor with args |
init(args) : base(b) | : base(b) chain | .ctor with explicit call BaseType::.ctor |
class E : Exception | class E : Exception | derived class with a BCL extends; base .ctor chained |
ex.Message (inherited BCL member) | ex.Message | callvirt Exception::get_Message, base-chain resolved |
init(...) on struct | — | ES3012: use composite literal / factory |
returns T clause | default return type for nested funcs missing -> T | — |
Interface conformance
Section titled “Interface conformance”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.
Field & property visibility
Section titled “Field & property visibility”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 x | FieldAttributes.Public |
x: int (bare field) | internal int x | FieldAttributes.Assembly |
priv let x: int (property) | private int X { get; init; } | private accessor methods; backing field private |
pub required let id: Guid | public 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 f | public | top-level TypeAttributes.Public / MethodAttributes.Public |
struct Point / func f (bare) | internal | TypeAttributes.NotPublic / MethodAttributes.Assembly |
pub class Inner (nested) | public nested | TypeAttributes.NestedPublic |
class Inner (bare nested) | internal nested | TypeAttributes.NestedAssembly |
priv class Inner / bare-default nested | private nested | TypeAttributes.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.
Properties
Section titled “Properties”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 => expr | T Area => expr; (computed) | get_Area only, no backing field |
let cur: T => self.inner.Current | forwarding computed getter | get_Cur only; body loads the forwarded member |
let x: T { get => expr } | authored getter | get_X only, no backing field |
var x: T { get => read() set(v) => write(v) } | behavioral property | authored 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 protocol | ordinary property + compiler-owned getloca_X companion |
var x: T { mut => &self.storage } | property plus direct mutation protocol | ordinary property + durable location companion |
let x: T { mut { ... yield &working ... } } | scoped read/modify protocol | property + opaque lease + __mut_begin_X / __mut_resume_X; consumer call enclosed in finally |
durable &object.property captured or live across await | receiver plus property access protocol | ordinary 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 contract | abstract get_X / set_X / ref-returning getloca_X + capability metadata |
external CLR ref T X / ref readonly T X | durable external property location | call 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&.
Nested types
Section titled “Nested types”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 body | nested enum Inner | nested System.Enum (Outer/Inner) |
struct Inner { … } in a type body | nested struct Inner | nested value type (Outer/Inner) |
class Inner { … } in a type body | nested class Inner | nested sealed class (Outer/Inner) |
union Inner { … } / ref union Inner { … } | nested union form | nested tag-enum+struct / abstract base + subclasses |
interface IInner { … } in a type body | nested interface IInner | nested interface (Outer/IInner) |
delegate func Inner(…) in a type body | nested delegate type | nested sealed MulticastDelegate (Outer/Inner) |
pub / bare / priv on a nested type | public / internal / private nested | NestedPublic / NestedAssembly / NestedPrivate |
Control flow, errors & match
Section titled “Control flow, errors & match”| E# | C# | IL |
|---|---|---|
let x = 42 | var x = 42; (immutable by E#) | stloc |
var x = 0 | var x = 0; | stloc |
for value in array | cached-length indexed loop | ldlen + direct ldelem; no enumerator/finally |
for i in start..end | cached-end counted loop | integer compare/branch; endpoints evaluated once |
a ? b : c | a ? b : c | conditional branch |
x ?? y | x ?? y | null check + branch |
x?.Member | x?.Member | null-conditional access |
match x { .a { } } | switch (x.Tag) | IL switch (jump table) |
match x { .a(v1, v2) { } } | multi-field destructure | field loads after tag check |
match x { .a(v) { } } (ref union) | is type pattern | isinst + field extraction |
let x = f()? | unwrap with early error return | conditional branch |
let x = f() else { return } | null guard, early return | brfalse |
defer { f() } | try { ... } finally { f(); } | exception handler block |
"hello {name}" | $"hello {name}" | String.Concat (value types boxed) |
Pointers, delegates & literals
Section titled “Pointers, delegates & literals”| E# | C# | IL |
|---|---|---|
&funcName | delegate*<...> | ldftn + calli |
&varName | ref varName | ldloca / ldarga / ldflda |
&object.locationProperty | borrow through the property’s declared protocol | getloca_ call or scoped lease/finally protocol; receiver evaluated once |
var p = &x | ref var p = ref x | ByReferenceType local |
&(int, int -> int) | delegate*<int, int, int> | FunctionPointerType |
x: *T param | ref T | managed pointer |
readonly *T param | in T | ByReferenceType + [In] |
out x: T param | out T | [Out] T& |
Span<T> / ReadOnlySpan<T> index | CLR ref-returning indexer | indexer call + ldobj/stobj; no boxing |
xs (a T[]) into a ReadOnlySpan<T>/Span<T> slot | (ReadOnlySpan<T>)xs | call op_Implicit(T[]) on the span |
s (a Span<T>) into a ReadOnlySpan<T> slot | (ReadOnlySpan<T>)s | call 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 · n → localloc → new Span<T>(void*, int) |
0xEDB88320 · 0b1010 | 0xEDB88320 · 0b1010 | ldc.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) = expr | temp + .Item1/.Item2 | field extraction |
derive equality | Equals / GetHashCode / == / != | generated methods |
static T { pub func +(a: T, b: T) -> T } | public static T operator +(T, T) | specialname hidebysig op_Addition |
derive debug | ToString() | 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>.
Concurrency & interop
Section titled “Concurrency & interop”| E# | C# | IL |
|---|---|---|
await expr | await expr | IAsyncStateMachine struct |
spawn { ... } | SpawnedOps.Spawn(() => { ... }) | Esharp.Stdlib.Spawned handle over Task.Run |
task func name(args) -> T | static 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 foreach | Esharp.Stdlib.AsyncStream channel-backed enumerator |
select { ... } | channel multiplex | Esharp.Stdlib.ChanSelect |
| structured-concurrency scope | scope owns children | Esharp.Stdlib.TaskScope |
List<int>() | new List<int>() | newobj List1 |
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 / decimal | same | reflected 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 sugar | fields + positional construction |
Implicit BCL namespace search
Section titled “Implicit BCL namespace search”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.LinqSystem.Collections.Generic System.DiagnosticsSystem.Collections.ObjectModel System.TimersSystem.Collections.Specialized System.ComponentModelSystem.Text System.ThreadingSystem.Text.Json System.Threading.TasksSystem.Text.Json.Serialization System.Threading.ChannelsSystem.IODisable the whole tier with <ImplicitUsings>disable</ImplicitUsings>, after which only explicitly
imported namespaces resolve.