Skip to content

Delegates & events

E# has two callable tiers, chosen by intent. Function pointers are specified in Functions; this page covers delegates and events.

TierFormCLRCost
function pointer&f, type &(int, int -> int)ldftn + callizero-alloc, single-target
delegateFunc/Action/EventHandler, delegate func, lambdasMulticastDelegate subclassheap, GC, multicast

A bare function name where a delegate type is expected converts to that delegate, bound directly to the real method (no synthesized forwarder, so reflection sees the actual target):

func dbl(x: int) -> int = x * 2
let f: Func<int, int> = dbl // ldnull; ldftn dbl; newobj Func`2::.ctor

It fires only when the target delegate type is known — a typed parameter, a typed let, a return type, or an event. An un-annotated let f = dbl is an error (ambiguous: delegate or function pointer?); write the delegate type, or &dbl for a pointer. It works for any delegate type, including bridging to a nominally distinct BCL delegate (let p: Predicate<int> = isEven). Lambdas convert the same way, with parameter types inferred from the target’s Invoke signature.

DelegateFuncDecl = "delegate" "func" TypeName "(" [ ParamList ] ")" [ ReturnType ] .

delegate func Name(...) mints a nominal delegate type — a sealed MulticastDelegate subclass, distinct from any structurally-identical Func<>. The emitted type is exactly what C# emits for delegate R Name(...), so it crosses the assembly boundary both ways. A Func<int,int> is not a BinOp even though both wrap (int) -> int — the same nominal philosophy as E#‘s interfaces. delegate is contextual (only before func at member scope).

EventDecl = "event" identifier ":" Type . // Type shall be a delegate

An event is a member — a controlled subscription point over a delegate. Events are declared field-style on ref data or interface only (they imply identity); an event on a value data is ES2140. The type after : shall be a delegate, else ES2141. event is contextual (only before name : in a type body).

Raise. raise Name(args) fires the event; it lowers to a thread-safe capture-then-invoke and is null-safe (a no-op with no subscribers). raise naming an event not declared on the enclosing ref data is ES2142. raise is contextual (the event name between raise and ( distinguishes it from a call).

Subscribe / unsubscribe with += / -=, on E#-declared and external (C#) events alike. The emitted CLR shape is exactly what C# emits for a field-style event — backing field, add_/remove_ accessors (lock-free Delegate.Combine/Remove + Interlocked.CompareExchange), and an EventDefinition — so a C# consumer subscribes with no glue, and E# subscribes to C# events the same way.