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.
Unsupported constructs and workarounds
Section titled “Unsupported constructs and workarounds”| Construct | Status | Workaround |
|---|---|---|
async keyword | n/a by design | uncolored — await promotes the function; the return type selects the shape |
explicit struct layout @offset | not a keyword | [StructLayout(LayoutKind.Explicit)] + [FieldOffset(N)] |
| ref union elements in chan<T> | limited | the subclass-ctor call path doesn’t resolve |
Mixed .es + .cs — known gaps
Section titled “Mixed .es + .cs — known gaps”.es and .cs fuse into one assembly with bidirectional references. Still limited:
- Cross-language method-impl bridge —
struct Foo : ICSharpInterfaceplus a receiver methodfunc (f: Foo) describe()writes theInterfaceImplementationmetadata 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.
Planned (near-term)
Section titled “Planned (near-term)”github/linguistregistration of E# (so.esis not mislabeled as ECMAScript at the source level).
Roadmap — design in flight
Section titled “Roadmap — design in flight”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.
Union types — union design idea
Section titled “Union types — union design idea”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 toobject). 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, andmatchdispatches viaisinst.matchover 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 bodiesA 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-lessInline 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>(…)).
Pattern matching
Section titled “Pattern matching”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.
Cadence
Section titled “Cadence”The corpus is the contract, and it grows fast — the working target is a couple hundred tests per working week.