Function declarations and calls
This page specifies the declaration and invocation surface shared by free functions, methods, static-facet members, constructors, and BCL calls. Receiver declarations are specified separately in Methods and static facets.
Grammar
Section titled “Grammar”FuncDecl = [ "readonly" ] "func" [ Receiver ] identifier [ Generics ] "(" [ ParamList ] ")" [ ReturnType ] ( Block | "=" Expr ) .ParamList = Param { Sep Param } [ Sep ] .Param = ( [ "out" ] [ "readonly" ] identifier ":" Type | identifier ":" "*" Type ) [ "=" Expr ] .ReturnType = "->" Type .ReturnsClause = "returns" Type .ArgList = Arg { Sep Arg } [ Sep ] .Arg = [ "out" | "&" | "*" ] Expr | identifier ":" Expr .Sep = "," | newline .FuncDecl without a Receiver declares a free function. At namespace scope it belongs to the namespace
host; in static Foo { ... } it belongs to that static facet; in a class body it is an in-body member.
The readonly prefix is meaningful only with a receiver. An implementation shall reject readonly func
without one.
Each parameter has a name and a type written in E#‘s name-first order. out, readonly, and *T are
the by-reference parameter family; their storage and CLR rules are in Pointers & by-ref.
The final = form is an expression body. It is equivalent to a block whose sole effect is return Expr
for a non-void function, or expression evaluation for a void function.
Return selection
Section titled “Return selection”A -> T return annotation fixes the function’s source return type. An omitted annotation is void-like
unless a containing class or static facet supplies a returns T clause. That clause applies only to
member functions in its own body that omit ->; an explicit arrow always wins.
static Parse { returns Result<int, string>
func port(text: string) = parsePort(text) // Result<int, string> func count(text: string) -> int = text.Length}returns is not an expression, a function annotation, or a namespace declaration. It is a default for
the enclosed member declarations only.
Default arguments
Section titled “Default arguments”A parameter default is part of the declaration:
func connect(host: string, port: int = 443, useTls: bool = true) -> string = "{host}:{port}:{useTls}"The default expression shall be a constant shape: a foldable literal, nil, or a composite literal,
dot-case, or Result construction whose inputs are themselves constant shapes. Any other default is
ES2180. A default is materialized at the call site, not allocated once at the
declaration; every omitted argument gets a fresh evaluation of its default expression.
A literal default is also represented in CLR metadata with [Optional] and a parameter constant. Thus a
C# caller sees the same trailing optional parameter rather than a language-only convention. Defaults that
have no CLR metadata representation still retain their E# call-site behavior.
Named arguments and argument order
Section titled “Named arguments and argument order”An argument without name: is positional. A named argument chooses the parameter with that exact name:
let endpoint = connect("api.example", useTls: false, port: 8080)At every call site — E# free function, method, constructor, : this, : base, or BCL member — all
positional arguments shall precede all named arguments. The named suffix may be written in any order.
Named arguments are not available for union or enum case construction.
The following errors are defined by the call shape:
| Diagnostic | Condition |
|---|---|
| ES2181 | a positional argument follows a named argument |
| ES2182 | a named argument names no parameter |
| ES2183 | required arguments are missing, or too many arguments are supplied |
| ES2184 | one parameter is supplied more than once |
Arguments are evaluated in parameter order, not their written order. In the call above the port
expression evaluates before the useTls expression because port precedes useTls in connect’s
signature. Omitted defaults materialize in that same order. This rule makes observable effects a property
of the callee’s signature rather than a cosmetic choice in a caller.
Overload selection
Section titled “Overload selection”E# resolves callable overloads by arity and supplied argument names, never by a better-conversion or argument-type ranking. Two candidates that have the same applicable parameter count and names are indistinguishable; a program shall not depend on a C#-style conversion preference to choose one.
For class constructors, same-arity init declarations are rejected at the declaration itself by
ES2185. Use a distinct arity, a default parameter, or one constructor that
performs the shared dispatch explicitly.
External overload sets are type-aware. The arity-and-names rule governs E#-declared callables, where
same-arity overloads are refused at the declaration. A referenced assembly’s overload set is not under
that rule and routinely holds several same-arity candidates that differ only by parameter type
(Interlocked.Exchange(ref int, int) beside (ref long, long)), so a call into one selects by argument
type. Two ranks, in order:
- An argument whose type is already settled — a typed binding, or
&place, which selects by the addressed place’s type — pins the candidate. - An unbound numeric literal only breaks ties among the candidates the settled arguments allow. A literal follows the overload the real arguments chose; it never chooses one.
The order is the whole rule. Ranking them equally lets Interlocked.Exchange(&slot, 11) on a long slot
select the int pair — the literal outvoting the receiver — which is metadata the verifier rejects, at a
call that reads as correct.
Generic calls
Section titled “Generic calls”FuncDecl may introduce generic parameters with <...>. A call may write its type arguments explicitly,
or leave them to the inference rules in Generics:
func pair<A, B>(left: A, right: B) -> (A, B) = (left, right)
let inferred = pair(1, "one")let explicit = pair<int, string>(1, "one")Inference is reified and follows the parameter/argument constraints, including target-typed lambda arguments. It does not turn overload resolution into type-ranking: arity and names still select the candidate before its generic arguments are closed.
Extension-method calls
Section titled “Extension-method calls”A C# extension method — a static method whose first parameter carries [ThisAttribute] — is callable
from E# through member syntax on its receiver, receiver.name(rest), exactly as C# calls it. This is
how the BCL’s extension surface is reached: LINQ (xs.Select(...), xs.Where(...)), the span helpers
(span.SequenceEqual(other), span.IndexOf(v)), and any third-party this-parameter method. The receiver
binds to the method’s first parameter, not to a hidden instance this; the remaining arguments follow
by the ordinary arity-and-name rule, and generic type parameters are pinned by the
inference algorithm with the receiver pinning first.
The receiver is an argument, so it takes the same implicit conversions an ordinary argument would at
that parameter — including the implicit span conversions.
A Span<T> or T[] receiver therefore flows into a this ReadOnlySpan<T> extension via op_Implicit:
let magic = stackalloc byte[](4) // Span<byte>readHeader(magic)if not magic.SequenceEqual(HeaderMagic) { // Span → ReadOnlySpan (op_Implicit) receiver; throw ModelFileException("bad header magic") // HeaderMagic (byte[]) → ReadOnlySpan arg}The receiver is passed by value; its address is taken only when the receiver slot is genuinely by-ref
(this ref / this in on the C# side).
The static form
Section titled “The static form”An extension is also callable on its host, with the receiver as argument zero:
Host.name(receiver, rest). This is the same method, reached the other way. An extension emits a plain
static method, every .NET language may call it that way, and E# reads its own extensions out of the same
metadata it hands everyone else — so both spellings resolve to one method and agree in every respect.
pub static Text { pub func (ext s: string) shout() -> string = s + "!"}
let loud = "hi".shout() // member formlet same = Text.shout("hi") // static form — one method, one answerlet n = Enumerable.Count(xs) // an imported C# extension through its own hostThe two spellings mirror each other across the language boundary. 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. Generic inference is unchanged: the receiver still pins first, it simply arrives as an argument.
E# declares extensions with an ext receiver in a static class, per-method or through an
ext (s: T) { ... } block — see Methods → extensions for the
declaration forms, the resolution tiers, and the CLR mapping. A plain receiver block at namespace scope
still declares an attached method, not a detached extension; ext is what
distinguishes them — and an attached method stays method-only (ES2142).
The static form belongs to ext, which is detached by definition, not to attachment. For an extension a
namespace-scope block hosts, the host is the namespace host, so its static form is the bare free call:
slug("A B") and "A B".slug() reach one method.
See also
Section titled “See also”- Functions — the complete overview, including method chaining.
- Methods and static facets — receivers and method-only calls.
- Pointers & by-ref —
out,readonly,*T,&, and*call arguments.