Skip to content

E#

A modern, composable, object-oriented language for the .NET CLR. Readable like Go, native to the runtime like C#, and a fraction of the surface area.

v0.1 · pre-alpha The language and compiler is currently pre-alpha. The compiler is functional for its test suite, and a small but growing set of real programs. I would not utilize the lnaguage in production yet — many small features are still awaiting implementation, and the compiler is still being hardened for robustness and performance.

The E# programming language is a general-purpose language that compiles to standard .NET assemblies, the same Common Language Runtime (CLR) that runs C#, F#, and Visual Basic. A .es file becomes a standard assembly .dll that any C# .NET project can reference without knowing it wasn’t C#. The file extension is .es and the build file is .esproj, though this may change in the future due to conflicting with the ECMAScript (JavaScript) build system in dotnet. E# it’s its own language that utilizes the Common Language Infrastructure (CLI) of the Common Language Runtime (CLR) to produce standard .NET assemblies that can be run anywhere the .NET runtime is available. The language is designed to be readable, maintain a smaller surface area than C#, and be enjoyable to work with.

In no order, the language draws inspiration from Swift, Go, Rust, C#, and Template Haskell. E# is a C-family language, procedular and object-oriented at its core, blending elements of functional programming where they fit.

namespace Demo
// `struct` is a value type: it copies on assignment, is compared by its fields,
// and has no implicit heap or identity.
struct Money { amount: int, currency: string }
// Utilize reciever-block form to attach `describe()` to money as an instance method
// Note: `describe()` will also be in the method set of `*Money
func (m: Money) describe() -> string = "{m.amount} {m.currency}"
// A `union` is a tagged union — the set of things that can happen, as data.
union ParseError {
empty
badNumber(text: string)
}
// Errors as values as a language / compiler feature.
func parse(s: string) -> Result<Money, ParseError> {
if s.Length == 0 { return error(.empty) }
var n = 0
if !int.TryParse(s, out n) { return error(.badNumber(s)) } // BCL interop is direct
return ok(Money { amount: n, currency: "USD" })
}

The premise is simple: the language you write in shapes how you think about the problem, and the more surface a language carries, the further the code drifts from the model it is meant to hold. E# keeps that surface small — what you write stays close to the shape of the problem, so a program fits in your head, reads back a month later without reverse-engineering the ceremony, and says the same thing in less code because there is usually one obvious way to say it. The better practices are meant to fall out of the grammar, not a style guide.

Concretely, it came from wanting a language the CLR didn’t have: a type system that reads like Go but goes further — generics, classes when you need them, and ideas borrowed from Rust (tagged unions you match exhaustively, errors as values, the -> return syntax). Concurrency shaped by Go and Swift. An object model that sits between Go and C#: more structure than Go, less ceremony than C#.

Being a first-class CLR citizen also opens a direction .NET never really had: compiling .es and .cs together into a single assembly — the JVM’s in-project polyglot story, on the CLR. This is one feature of the compiler workspace, not the primary way to use the language; E# stands on its own and emits ordinary assemblies. We aim for the full JVM level of support and robustness here and are still exploring it in depth — the mechanism is a staged joint compilation, described below.

The aim isn’t more features than C#; it’s fewer, composed well: value types by default, errors as values, uncolored async, methods that attach to types instead of being trapped inside them. The surface stays small less by leaving things out than by rarely offering the same thing twice — two ways to do something are usually two slightly different things. And it stays a C-family language throughout: imperative and object-oriented, with the functional tools at hand but not at the wheel — closer in temperament to C# than to F#.

Native to the CLR

Compiles through Mono.Cecil to ordinary IL. E# types are indistinguishable from C# types to a consumer — reference the .dll and call them. Interop →

Value-first

struct is a value type and class is a reference type. Each keyword names the CLR form it emits. Types →

Errors as values

Result<T, E> and ? propagation for in-language flow; try/catch only at the BCL boundary. Errors →

Uncolored async

Any await promotes its function — no async keyword. Uncolored by default; semi-colored when you want it, since the return type picks the machinery. Concurrency →

E# is general-purpose — it covers the same ground as any other CLR language, with no single intended niche. Its primary shape is standalone. Mixing .es and .cs in one project is a further feature of the workspace — one we are taking toward full JVM-level robustness and still actively exploring — rather than the intended primary path.

The corpus is the most honest picture of what compiles today: a large set of real .es programs drawn from the language’s own test suite and samples — passing test cases, showcases, and integration tests — each one compiled cleanly (or rejected with its expected diagnostic) and either producing its asserted output or standing as a real, well-formed program.

A mixed .esproj is one project the E# compiler workspace drives end to end, in stages — the same shape the JVM uses to compile Kotlin and Java together, adapted to the CLR. Each language is compiled against the other’s declarations first and its compiled output second:

  1. Parse both halves. The workspace parses the .es sources itself and hands the .cs sources to Roslyn.
  2. Declare the C# side. Roslyn runs a declaration-level pass over the .cs, resolving its types, base classes, interfaces, and member signatures. The workspace projects those declared types into E#‘s own symbol table as language-neutral handles — so E# code can name, call, extend, and derive from a C# type.
  3. Compile and emit the E# half. The E# binder resolves against those handles, lowers, and emits its half to an assembly in memory. References to C# types are written as forward references — the C# half does not exist yet, so they are names to be resolved at the join.
  4. Compile the C# half against it. Roslyn takes the just-emitted E# assembly as a reference and compiles the .cs, now resolving the E# types it uses.
  5. Join and verify. The two assemblies are merged into one with a single identity, and the result is run back through metadata verification before it is written.

One workspace orchestrating two compilers and joining their outputs — no fork of Roslyn or the E# compiler, just a staged pipeline. This is the layer we are still hardening toward JVM-level completeness, and where the rough edges live today. See a worked example — a C# base, an E# type extending it, and a C# type extending that, in one project.

Some of this leans on Roslyn behaviors it was not specifically designed to provide — the same kind of unsanctioned platform use early Kotlin made of the JVM — which is part of why this layer is experimental rather than guaranteed.


The name: E# is the major third above C# — a sibling in the same key. It was available. No deeper claim than that.