Skip to content

Methods and static facets

E# makes method attachment explicit. A function becomes a method only by declaring a receiver block; the first ordinary parameter of a free function never becomes a receiver implicitly. This page specifies the receiver grammar, the type-facet rule, and the resulting call and metadata shapes.

FuncDecl = [ "readonly" ] "func" [ Receiver ] ( identifier [ Generics ] | OperatorName )
"(" [ ParamList ] ")" [ ReturnType ] ( Block | "=" Expr ) .
Receiver = "(" identifier ":" [ "static" ] Type ")" .
StaticFacetDecl = "static" TypeName [ Generics ] "{" { StaticMember } "}" .
OperatorName = "+" | "-" | "!" | "~" | "*" | "/" | "%" | "&" | "|" | "^"
| "<<" | ">>" | ">>>" | "==" | "!=" | "<" | ">" | "<=" | ">=" .
MethodCall = Expr "." identifier "(" [ ArgList ] ")" .

static is contextual in a receiver: func (c: static Counter) reset() is distinct from a parameter whose type happens to contain the word. The grammar admits a pointer type after static so the parser can recover, but a static pointer or readonly static receiver is ill-formed under the rules below.

namespace Geometry
struct Circle { r: float }
func (c: Circle) area() -> float = c.r * c.r * 3.14159

A receiver block is legal in exactly two positions, and which one it is in decides what it means:

PositionFormMeaning
namespace scopefunc (c: Circle) area()a method attached to a type this namespace owns — gated
static class body, marked extfunc (ext s: string) shout()an extension over a type anyone owns — gate lifted
static class body or namespace scopeext (s: string) { ... }an extension block — the same extensions, receiver declared once

Nowhere else. A receiver block in a class / struct / union body is ill-formed — a type body declares self-based members, not receiver blocks — and inside a static class an unmarked receiver block is ES2289, as is per-method ext at namespace scope (the block form is the one namespace spelling, so a bare receiver block keeps exactly one meaning there). The attached form and the ext forms are not variants of one rule: the first is gated on ownership and emits an instance method, the others lift that gate and emit a static [Extension], so letting either spelling silently mean the other would make ownership a matter of where a declaration happened to be written.

The receiver type shall be declared in the method’s own namespace. Files do not create a second boundary: all files in one namespace contribute to the same method set, so an attachment may appear in another file or before the type’s textual declaration. A receiver over a closed generic type, such as func (h: Holder<int>) get(), is ES2132; attach to the open form and make the method generic as necessary.

An attached receiver method is method-only. The only invocation form is receiver.name(...); area(c) is ES2142. Conversely, func area(c: Circle) declares a free function and shall be called area(c), not c.area(). This distinction is entirely syntactic and never inferred from the first parameter’s type. An ext method is the one exception, and it is not an exception to this rule but a consequence of a different one — it is detached, and it emits a static method its host publishes to every .NET language. See the static form below.

A member call receiver.name(args) resolves solely within the receiver type’s method set — the methods attached to the receiver’s own type identity plus those inherited up its base-class chain — with the receiver’s type arguments substituted into the selected method’s signature. The candidate is chosen by the ordinary arity-and-argument-name rule; the call’s type is the return type of that selected method.

The receiver’s method set and the free-function namespace are disjoint. A same-named free function in scope never participates in a member call, and a same-named method on an unrelated type never participates in a free call — the two are separated by the method-only rule above, not merged into one bare-name table. So a receiver method Read and a free function Read coexist without collision, and s.Read(...) binds to the former’s signature — including its return type — regardless of any Read reachable as a free function:

namespace Codec
func Read(tag: int) -> Section = lookup(tag) // free function
func (s: *Reader) Read(n: int) -> int { // method on Reader — same name, disjoint set
let got = s.fill(n) // got : int, from THIS Read's return type,
return got // never from the free Read above
}

Inheritance walks the base chain: a method attached to a base type is available on a derived receiver, and a generic receiver closes the method’s type parameters from the receiver’s own type arguments — holder.get() on a Holder<int> returns int because the receiver’s T is int.

DeclarationReceiver meaningAvailable on
func (c: T) f()value receiver; a struct is copied, a class reference is copiedT and *T
func (c: *T) f()mutable managed-pointer receiver*T only
readonly func (c: T) f()read-only in receiverT and *T
func (c: static T) f()compile-time alias for an explicit static facetT.f() only
func (ext c: T) f()extension receiver — the CLR this T first parameterany T in scope of the facet

A value receiver on a struct works on a snapshot. Writing its fields changes that snapshot, not the caller’s variable. A pointer receiver accesses the caller’s storage and is the form for in-place mutation or avoiding a large struct copy. *Class is ill-formed because a class already has reference semantics. A readonly receiver may read but shall not write through the receiver; on a struct it emits the CLR in this/[IsReadOnly] form.

static Foo { ... } explicitly declares the static facet of the non-generic type identity Foo. It contains constants, static state, computed properties (let x: T => e, the same member form the namespace host declares — a static get_x that recomputes e on every read, with no storage), functions, a returns clause, and nested types — never a bare statement. A static facet may stand alone, or it may share an identity with a class Foo or struct Foo companion. The companion contributes instance members; the facet contributes static members; both emit into the one CLR type when they have the same name and arity.

class Counter { var value: int }
static Counter {
var total: int = 0
}
func (c: Counter) read() -> int = c.value
func (c: static Counter) resetTotal() -> int {
c.total = 0
return c.total
}

The ordinary receiver selects the instance facet when both facets exist, so counter.read() is an instance call. The explicit static receiver selects the static facet, so Counter.resetTotal() is a static call. For a type with only static Foo, an ordinary func (f: Foo) selects that facet automatically; the ordinary spelling keeps a standalone utility type concise.

A static receiver is an alias, not a value. It introduces no hidden CLR parameter, has no address, cannot be a pointer or readonly, and is not a route to interface conformance. It may name fields and static methods in the facet body. If no static Foo { ... } has been declared, func (f: static Foo) is ES2211: declare static Foo { ... }, or attach the method as an instance method without the static keyword. Pointer and readonly static receivers are ES2212.

An operator function belongs to a class or struct’s companion static facet, either directly or through a static receiver attachment. Standalone utility static facets and ordinary type/interface bodies cannot own operators.

struct Vec2 { x: double, y: double }
static Vec2 {
pub func +(left: Vec2, right: Vec2) -> Vec2 = Vec2 {
x: left.x + right.x
y: left.y + right.y
}
}
pub func (v: static Vec2) -(value: Vec2) -> Vec2 = Vec2 { x: -value.x, y: -value.y }

The static receiver is not an operand and is omitted from CLR metadata. At least one explicit operand must have the owner’s open type identity. Unary functions take one operand; binary functions take two. A shift’s right operand is int; equality and ordering return bool; ==/!=, </>, and <=/>= must be declared in pairs with identical ordered parameter types. Operator functions are synchronous, non-generic methods with no defaults, out, params, pointer/by-ref operands, or void result. Generic owner parameters remain available through static Wrapper<T>.

Visibility is explicit for operators: pub func exports a public CLR operator, while an unmarked func is assembly-internal even inside pub static T. Symbolic functions are invoked only by operator expressions, never as Vec2.+(...). derive equality conflicts with an explicit ==/!= pair.

Method chaining is ordinary repeated MethodCall selection; it has no separate dispatch rule. A class method that returns its receiver threads one object through the chain, while a value-struct method returns the next value:

struct Vec { x: int, y: int }
func (v: Vec) add(other: Vec) -> Vec = Vec { x: v.x + other.x, y: v.y + other.y }
func (v: Vec) scaled(k: int) -> Vec = Vec { x: v.x * k, y: v.y * k }
let result = (Vec { x: 3, y: 2 }).add(Vec { x: 1, y: 4 }).scaled(4)

A leading . continues a chain across a newline. A chain whose intermediate result is Result<T, E> does not automatically enter its success payload; unwrap it with ? or bind the value before continuing.

Source receiverEmitted target
value class receiverordinary instance CLR method
value struct receiverinstance method with value this
pointer receiverstatic host accepting managed-pointer T&
readonly receiverinstance in this / [IsReadOnly] shape
static receiverstatic method with no receiver parameter

The pointer-receiver static host is an implementation detail. Its source invocation remains method-only, and it cannot be called with a free-function spelling. The exact pointer representation is defined by Pointers & by-ref.

An extension is a receiver-block func marked ext, hosted by a standalone static class (static Text { ... } with no companion type) or — through the ext block — by the namespace host. A static class is already a CLR static class, and a receiver block already means “this parameter is the receiver”. ext says which of the two meanings a receiver block has (receiver-block placement), and lifts the namespace gate so the receiver type may belong to anyone.

static Text {
pub func (ext s: string) shout() -> string = s.ToUpper()
pub func (ext s: string) slug() -> string = s.ToLower().Replace(" ", "-")
}
"Hello World".shout() // "HELLO WORLD"
Text.shout("Hello World") // the same method, on its host

An extension emits a plain static method on its host, so Host.name(receiver, rest) calls it with the receiver as argument zero. Both spellings reach one method and agree in every respect — see Calls → the static form. This is the mirror of C#: C# writes the receiver first in the parameter list and hides it at the member call, E# writes it outside the parameter list in a receiver block and supplies it at the static call. An imported C# extension is reachable the same way through its own host (Enumerable.Count(xs)), because it is the same metadata.

ext composes with the existing receiver kinds rather than replacing them — the receiver kind says how the receiver is passed, ext says whose type it is:

Declaration (in a static facet)ReceiverCLR first parameter
func (ext s: T) f()valuethis T
readonly func (ext s: T) f()read-onlythis in T
func (ext s: *T) f()by-refthis ref T

The host shall be a CLR static class — a standalone static Text { ... }, or the namespace host through the block form. A static facet written beside a same-named class or struct merges into it, and the merged type is not a CLR static class — every other .NET language filters extension hosts by abstract sealed, so the method would be reachable from E# by member syntax and invisible everywhere else. That is ES2291. An ext receiver that is itself a static facet is ES2290: an extension extends a value, and a static facet has none.

A member call recv.name(args) resolves in two ordered tiers:

  1. The receiver type’s own member set, including members inherited up its base chain. An instance member always wins.
  2. Only if tier 1 has no match: extensions in scope — ext members of static classes and namespace hosts in the current namespace or a namespace imported with using, plus [Extension] methods from referenced assemblies — whose receiver type accepts recv by identity, a base class, or an implemented interface. The most specific receiver wins, so an extension on string is preferred over one on object. An extension-property read recv.name resolves through the same two tiers.

Within tier 2, an extension declared in the current compilation is nearer than one merely visible in a referenced assembly, and wins. Dispatch is static: an extension is never virtual and never overrides, so a same-named instance member hides it and adding an extension cannot change what existing code means.

An extension emits the standard convention, and all three parts of it, because C#‘s discovery uses all three: System.Runtime.CompilerServices.ExtensionAttribute on the assembly (which names candidate hosts), on the facet class (which filters them), and on the method (which claims parameter 0 as the receiver). A C# consumer then calls s.shout() and sees it in completion, exactly as it would a C# extension. Emitting fewer than three produces a static method that E# can call by member syntax and no other language can see.

Each position carries the attribute once. The attribute states a fact about its target and does not count what caused it: a facet with three ext members is one host, and an assembly with two such facets contains extensions once.

An ext block declares the receiver once for a set of extensions. It is sugar: each member means exactly the per-method form, with the block’s receiver as parameter 0, and the two spellings may mix freely in one host.

pub static Text {
ext (s: string) {
pub func shout() -> string = s.ToUpper()
pub func slug() -> string = s.ToLower().Replace(" ", "-")
pub let words: int => s.Split(' ').Length // extension property
}
}
"hello world".shout() // "HELLO WORLD"
"hello world".words // 2
Text.shout("hello world") // the static form, unchanged

The receiver kinds compose on the block header as they do per-method: ext (s: T) passes the receiver by value, readonly ext (s: T) as in, and ext (s: *T) by reference. A static facet as the block receiver is ES2290, reported once on the header. The block itself takes no visibility — each member carries its own pub/priv. Multiple blocks with different receivers in one host are allowed and are the point.

A block holds func members (including task func) and computed get-only properties, let name: Type => e. Nothing else: an extension has no per-receiver storage, so stored state, var, const, operators, nested types, returns, and a member that writes its own receiver are ES2298. A block in a type body is ES2299.

The namespace host may host a block. The namespace host is already the CLR static class every free function lives on, so ext (s: T) { ... } at namespace scope emits its extensions there. This is the one placement the per-method spelling does not have — per-method ext at namespace scope stays ES2289, so a bare receiver block at namespace scope keeps its single attachment meaning. For a namespace-hosted extension the host-static form is the bare free call: name(recv, args) reaches the same method as recv.name(args). This is the one ext exception to the method-only rule, and it is the same fact as Host.name(recv) — the host is simply unnamed in source.

namespace Util
ext (s: string) {
pub func slug() -> string = s.ToLower().Replace(" ", "-")
}
"A B".slug() // member form
slug("A B") // the namespace host's static form

let name: Type => e in an ext block is a computed, get-only extension property. The read recv.name resolves in the same two tiers as a method call — an instance member always wins, the most specific receiver wins among extensions — and binds as the host’s static getter call. The property is never assignable, and the result type annotation is required (ES1018).

A property emits its getter as a plain static method on the host — get_name(receiver), callable from any language — plus the C# 14 extension-member skeleton, so a C# consumer reads it as a property, not a method: a nested marker group on the host carries a parameterless skeleton get_name, a PropertyDefinition, and an [ExtensionMarker] naming the sibling marker type whose <Extension>$ method records the receiver. E# reads only its own convention today: extension properties declared in referenced assemblies through the C# 14 shape are not yet imported.