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.
Grammar
Section titled “Grammar”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.
Attachment and namespace gate
Section titled “Attachment and namespace gate”namespace Geometry
struct Circle { r: float }func (c: Circle) area() -> float = c.r * c.r * 3.14159Receiver-block placement
Section titled “Receiver-block placement”A receiver block is legal in exactly two positions, and which one it is in decides what it means:
| Position | Form | Meaning |
|---|---|---|
| namespace scope | func (c: Circle) area() | a method attached to a type this namespace owns — gated |
static class body, marked ext | func (ext s: string) shout() | an extension over a type anyone owns — gate lifted |
| static class body or namespace scope | ext (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.
Member-call resolution
Section titled “Member-call resolution”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.
Receiver kinds and method sets
Section titled “Receiver kinds and method sets”| Declaration | Receiver meaning | Available on |
|---|---|---|
func (c: T) f() | value receiver; a struct is copied, a class reference is copied | T and *T |
func (c: *T) f() | mutable managed-pointer receiver | *T only |
readonly func (c: T) f() | read-only in receiver | T and *T |
func (c: static T) f() | compile-time alias for an explicit static facet | T.f() only |
func (ext c: T) f() | extension receiver — the CLR this T first parameter | any 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 facets
Section titled “Static facets”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.valuefunc (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.
Operator functions
Section titled “Operator functions”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.
Fluent calls
Section titled “Fluent calls”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.
CLR mapping
Section titled “CLR mapping”| Source receiver | Emitted target |
|---|---|
| value class receiver | ordinary instance CLR method |
| value struct receiver | instance method with value this |
| pointer receiver | static host accepting managed-pointer T& |
| readonly receiver | instance in this / [IsReadOnly] shape |
| static receiver | static 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.
See also
Section titled “See also”- Function declarations and calls — parameters and method arguments.
- Declarations — in-body members and static-facet contents.
- Names & resolution — namespace-local attachment and imports.
Extensions
Section titled “Extensions”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 hostThe static form
Section titled “The static form”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) | Receiver | CLR first parameter |
|---|---|---|
func (ext s: T) f() | value | this T |
readonly func (ext s: T) f() | read-only | this in T |
func (ext s: *T) f() | by-ref | this 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.
Resolution
Section titled “Resolution”A member call recv.name(args) resolves in two ordered tiers:
- The receiver type’s own member set, including members inherited up its base chain. An instance member always wins.
- Only if tier 1 has no match: extensions in scope —
extmembers of static classes and namespace hosts in the current namespace or a namespace imported withusing, plus[Extension]methods from referenced assemblies — whose receiver type acceptsrecvby identity, a base class, or an implemented interface. The most specific receiver wins, so an extension onstringis preferred over one onobject. An extension-property readrecv.nameresolves 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.
CLR mapping
Section titled “CLR mapping”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.
The ext block
Section titled “The ext block”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 // 2Text.shout("hello world") // the static form, unchangedThe 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 formslug("A B") // the namespace host's static formExtension properties
Section titled “Extension properties”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.