Extension members
Extensions provide helper members for an existing receiver type without
modifying the original declaration. The extension keyword declares this
construct.
An extension declaration is a namespace-level container that targets a
specific type via a for clause. Importing the container brings its members
into scope for lookup.
An extension declaration may omit its identifier, in which case the
compiler synthesizes a private, mangled container name. Public extensions and
extensions imported by name must declare an explicit identifier so the
container can be referenced. Namespace-level extension declarations default to
internal; public explicitly exports a named extension from the assembly.
Applying fileprivate keeps the declaration file-local and mangles the emitted
metadata name even when the source uses an explicit identifier.
Extensions may declare type parameters and generic constraints. These constraints participate in extension resolution: an extension is applicable only when its type parameters can be inferred and all declared constraints are satisfied for the receiver type. Extensions whose constraints are not met are ignored during member lookup.
Examples:
extension StringExt for string {
func ToSlug() -> string => self.Trim().ToLowerInvariant().Replace(" ", "-")
}
import MyApp.StringExt.*
val slug = " Hello World ".ToSlug()
extension for string {
func IsNullOrWhiteSpace() -> bool => self.Trim().Length == 0
}
val empty = " ".IsNullOrWhiteSpace()
// Constrained extension: applicable only when the receiver satisfies the constraints.
extension ValueSequenceExt<T> for System.Collections.Generic.IEnumerable<T>
where T: struct {
func Sum() -> T { /* ... */ }
}
// If the receiver's element type is not a struct, this extension is ignored during lookup.
// Extension property: participates in member lookup like an instance property.
extension ListIntExt for System.Collections.Generic.List<int> {
var CountPlusOne: int {
get => self.Count + 1
set => self.Add(value)
}
}
import System.Collections.Generic.*
import MyApp.ListIntExt.*
val items = List<int>()
items.Add(1)
val c = items.CountPlusOne // invokes getter
items.CountPlusOne = 5 // invokes setter
// Static extension members: accessed through the receiver type when no real static member matches.
extension ListStatics for System.Collections.Generic.List<int> {
static func Empty() -> System.Collections.Generic.List<int> => System.Collections.Generic.List<int>()
static DefaultCapacity: int { get => 4 }
}
import System.Collections.Generic.*
import MyApp.ListStatics.*
val empty = List<int>.Empty()
val cap = List<int>.DefaultCapacity
Each member inside the body is implicitly an extension member for the receiver
type. Members may be function declarations or computed properties. The compiler
synthesizes a self parameter whose type matches the receiver and passes it as
the first argument whenever the member is invoked. The self parameter behaves
like a val binding: it cannot be reassigned but may be used to access members
or forwarded to other calls. Extension members default to public
accessibility and may be marked internal to restrict their visibility; other
modifiers are rejected. As a result, extensions cannot declare protected or
private members.
Extension methods
Extension methods add callable helpers to the receiver type. They are declared
inside an extension container as function members:
extension StringExt for string {
func ToSlug() -> string {
// inside the body, `self` is a synthesized parameter of type string
return self.Trim().ToLowerInvariant().Replace(" ", "-")
}
}
The receiver parameter determines which expressions may invoke the extension.
Additional parameters follow the ordinary parameter rules: they may be generic,
optional, params, or accept lambdas. Generic receiver parameters are
substituted during method type inference, so helpers like Where<T>(this IEnumerable<T>, Func<T, bool>) become available to Raven code as soon as the
appropriate namespace is imported.
Partial explicit type arguments
When calling a generic method (including extension methods), Raven permits supplying only the trailing type arguments that cannot be inferred.
- The receiver's type is inferred from the call-site receiver expression.
- Any remaining type parameters are inferred from the provided arguments.
- Explicit type arguments provided at the call site are right-aligned with the method's type parameter list.
This mirrors the common C# ergonomics for extension calls where the receiver type is already known.
import System.Console.*
import System.Collections.Generic.*
val items = List<int>()
items.Add(1)
items.Add(2)
extension MyEnumerableExt<T> for System.Collections.Generic.IEnumerable<T> {
func CountItems<B>(arg: T) -> B {
return default(B)
}
}
// T is inferred from the receiver (`IEnumerable<int>`), while B is specified explicitly.
val count2 = items.CountItems<double>(2)
WriteLine(count2)
// The fully specified form is still valid.
val count3 = items.CountItems<int, double>(2)
WriteLine(count3)
If the supplied explicit type arguments do not match the method's trailing type parameters (or if inference cannot determine the remaining parameters), overload resolution fails and a diagnostic is reported.
Extension methods participate in lookup through the same import mechanism used
for types. Importing a namespace brings every extension method declared on the
static types within that namespace into scope. Importing a specific static type
exposes only the extensions declared on that type. Metadata extensions contained
in referenced assemblies, such as System.Linq.Enumerable, and Raven-authored
extensions are surfaced uniformly by binding, so source and metadata callers see
the same candidates.【F:src/Raven.CodeAnalysis/Binder/NamespaceBinder.cs†L33-L61】
Extension classification is determined per member rather than per container. This matters for mixed extension containers and imported metadata. A single extension container may therefore contribute both:
- classic instance extension members, which participate in instance-style extension lookup and behave like ordinary extension methods/properties, and
- static extension members, which participate only in static member lookup on the receiver type.
Invoking an extension uses method-style member access. When expr.Member(...)
fails to resolve to an instance member, Raven gathers the in-scope extension
methods whose receiver parameter is compatible with expr. Eligible extensions
join overload resolution alongside instance members; if both an instance member
and an extension are applicable, the instance member wins. Once overload
resolution selects an extension, the compiler rewrites the invocation to pass the
receiver as the leading argument to the static method before lowering, so the
generated IL matches the direct static-call form.【F:src/Raven.CodeAnalysis/Binder/BlockBinder.cs†L1946-L2001】【F:src/Raven.CodeAnalysis/BoundTree/Lowering/Lowerer.Invocation.cs†L8-L29】
Because extension methods are ordinary static methods, accessibility rules and
diagnostics mirror those for other members. Missing imports or inaccessible
extensions produce the same RAV1xxx diagnostics emitted for ordinary method
lookups. Lambdas supplied to extension method parameters participate in the same
delegate inference as other calls; Raven replays the lambda for each candidate
signature until one succeeds.
Extension properties
Extension properties participate in member lookup alongside methods. When a
property access such as expr.Member fails to bind to an instance property,
Raven considers imported extension properties whose synthesized self
parameter can accept the receiver. Successful binding rewrites the access to
call the accessor extension method (for example, get_Member(self)), and
assignments translate to setter invocations that pass both self and the
assigned value. Overload resolution prefers instance properties over
extensions, mirroring the method rules. Extension properties are accessor-only:
they cannot declare backing storage, and both accessors must be implemented
with bodies or expression clauses.
extension ListExt for List<int> {
var CountPlusOne: int {
get => self.Count + 1
set => self.Add(value)
}
}
Static extension members
Extensions may also declare static methods and properties. Static extension
members are associated with the receiver type and are accessed through static
member lookup (Type.Member) or by importing the target type (import Type.*). When binding a static member access, the compiler first resolves real
static members on the type; if no match is found, it searches in-scope extension
containers whose receiver type is compatible with the target type. Receiver
compatibility follows the same implicit conversion and nullability rules used
for extension methods, so constructed and nested types participate normally.
Static extension members do not synthesize self and are emitted as ordinary
static members on the extension container. Binding classifies them separately
from classic extension methods even when they live in the same extension
container. As a result, a mixed container may expose value.Member(...)-style
instance extensions and Type.Member(...)-style static extensions at the same
time without forcing one model onto the other.
Pipeline syntax is documented in Pipe expressions.