Skip to content

Limitations & roadmap

E# is pre-alpha. The language is real and tested — every assembly the harness emits passes ILVerify — but the surface still moves. This page states the gaps normatively rather than implying completeness.

ConstructStatusWorkaround
async keywordn/a by designuncolored — await promotes the function; the return type selects the shape
explicit struct layout @offsetnot a keyword[StructLayout(LayoutKind.Explicit)] + [FieldOffset(N)]

| ref union elements in chan<T> | limited | the subclass-ctor call path doesn’t resolve |

.es and .cs fuse into one assembly with bidirectional references. Still limited:

  • Cross-language method-impl bridgestruct Foo : ICSharpInterface plus a receiver method func (f: Foo) describe() writes the InterfaceImplementation metadata but not yet the body forwarder.
  • Union-from-C# — C# reads the discriminator and payload accessors but pattern-matches with a C# switch, not E# match (by design).
  • Generic constraints across the boundary — simple cases work; nested/numeric-constrained generics may surface gaps.
  • github/linguist registration of E# (so .es is not mislabeled as ECMAScript at the source level).

The sections below are proposals: designed and argued against the compiler, but not committed — shapes will move and some may drop. The substantial sections are the firm, load-bearing direction; the short list at the end is sketched in passing.

The largest item — and the one where today’s spec is already strong, so it’s worth being precise about the baseline.

Today. E# has two union kinds, and they already do a lot (see Types):

  • value union — a tag + payload struct with factory methods, dot-case shorthand, multi-payload cases, and reified generics (Option<int> is a real closed generic struct, not erased to object). Always a struct.
  • ref union — an abstract base with a sealed subclass per case (Outer_case), for recursive / polymorphic shapes (ASTs, trees); it carries identity and a shared base, and match dispatches via isinst.
  • match over either is exhaustiveness-checked, usable as an expression, and already supports multi-payload positional binding and the transparent single-payload case view.

What a union cannot do today: carry its own methods (a receiver block attaches to struct / class, not union), conform to an interface, hold a case that is an existing type, or be written inline as A | B. And the value-union layout is naive — SequentialLayout carrying every case’s payload at once (a 5-case union is as wide as all five payloads combined).

The direction closes exactly those gaps, making union the one union primitive — tagged, discriminated, and pseudo-anonymous, that also carries members and conforms to interfaces.

Members & methods on a union. In-body methods, plus receiver blocks extended to union receivers — so func (s: Shape) area() attaches as shape.area(), the same attachment struct gets today:

union Shape : IShape {
Circle, Square
func area() -> int = match self { // in-body method satisfies IShape.area()
.Circle(c) { c.r * c.r * 3 }
.Square(s) { s.s * s.s }
}
}

Interface conformance (nominal). A union : I conforms two ways: own methods (in-body or receiver), or auto-forward — when every member already conforms, the compiler synthesizes the match-and-dispatch:

struct Circle : IShape { r: int }
struct Square : IShape { s: int }
union Shape : IShape { Circle, Square } // verified: each member : IShape → IShape forwarded, no bodies

A value union boxes at the interface boundary — so conformance is “potentially not by direct dispatch”; reach for ref union when it’s hot. Conformance stays nominal and exact (consistent with the shipped struct/class rule).

Type-member cases — a case that is an existing type (your struct/class, or an external CLR type), mixable with inline cases:

union Event { Click(MouseEvent), key(code: int), closed } // type-member + inline + payload-less

Inline A | B in any type position, desugaring to an anonymous union — the pseudo-anonymous form:

func format(v: int | string | bool) -> string = match v {
.int(n) { "n={n}" } .string(s) { s } .bool(b) { b ? "on" : "off" }
}

The headline — composable error sets. Two functions with different error types compose under ? with no wrapper union and no .MapErr, because ? widens each error into the declared set (an explicit into / derive convert covers genuine cross-type conversions):

func load(id: Guid) -> Result<User, DbError | NetError> {
let row = dbFind(id)? // DbError widens into the set
let prof = netFetch(url)? // NetError widens in too
return ok(build(row, prof))
}

It earns the name with a real layout — overlapped (not summed) payloads, niche-filling when a payload is a nullable ref or *T, tagless for ref-only unions — common fields shared across value-union cases (a ref union already gets this today through its base class), and a doubling as a generic type-set bound (func max<T: int | long | double>(…)).

Pulled along by the union work, and already half-landed (transparent single-payload case views ship today). The rest is field-level binding, so an arm reads in domain terms instead of accessor noise:

match o {
.limit { side, qty, price } { qty > 0 && price > 0.0 } // not .limit(l), then l.qty …
.market { side, qty } { qty > 0 }
}

Plus positional deconstruct (.point(x, y)), guards (.Circle(c) if c.r > 100), and or-patterns (.Circle | .Square). Field patterns are pure sugar — identical IL to the whole-value-plus-.field arm.

The corpus is the contract, and it grows fast — the working target is a couple hundred tests per working week.