Functions

Functions may exist directly inside a namespace. These free functions provide behavior without requiring a custom type or an object instance.

Free functions

A free function belongs to its namespace rather than to a struct. It has no implicit this value and may be called directly wherever its name is in scope.

namespace Xenon.Math;

public float Clamp(float value, float minimum, float maximum)
{
    if (value < minimum) return minimum;
    if (value > maximum) return maximum;
    return value;
}

From another namespace, import it with using Xenon.Math; or call it by its qualified name: Xenon.Math.Clamp(value, 0.0f, 1.0f).

Free functions are a natural fit for operations that do not belong to a particular object. Behavior tied to one struct instance is expressed as a method instead.

Declaration and calls

int Add(int a, int b)
{
    return a + b;
}

int result = Add(20, 22);

Arguments are checked against parameter types and parameters receive values from the caller. A struct parameter receives an independent copy of the struct's fields. Pointer fields are copied as pointer values; the referenced memory is not deep-copied.

Function and method overloads

Free functions, instance methods, static methods and interface methods may share a source name when their parameter signatures differ. Parameter types retain their full Xenon identity, including char, pointer/reference readonly capability and ownership wrappers.

struct ConsoleWriter
{
    public void Write(int value) { }
    public void Write(char value) { }
    public void Write(readonly byte* value) { }
}

writer.Write(42);       // Write(int)
writer.Write('A');      // Write(char)
writer.Write("Health"); // Write(readonly byte*)

The compiler collects the visible overload set and applies implicit-conversion rules. An exact match is better than a standard conversion, which is better than a user-defined implicit conversion. A conversion sequence can contain at most one user conversion. A candidate with an invalid conversion is ignored; overload lookup never introduces a numeric coercion that ordinary assignment would reject. If no candidate matches, or equally good candidates remain, the call is rejected with an overload diagnostic.

The generic arity and parameter types distinguish declarations. Parameter names and return type do not: int Parse(readonly byte* text) and long Parse(readonly byte* value) are duplicate signatures. When a non-generic exact overload and an inferred generic exact overload are otherwise tied, the non-generic declaration wins. A generic candidate whose inference or constraints fail is not applicable.

Inherited overloads remain visible, while an override replaces only the base virtual slot with the same callable signature. A readonly receiver can select only a receiver-compatible method. Taking an overloaded function address uses the expected function-pointer signature, as in function int(int)* callback = &Transform;; without enough type context, multiple matching functions are ambiguous.

C ABI names are not overloadable

Ordinary Xenon overloads receive distinct managed native identities and remain distinct through XELIB metadata. extern declarations use their identifier as the C symbol, and export declarations use the flattened namespace name. Two overloads that would expose the same C ABI symbol are rejected; choose distinct source/native names at that boundary.

Return values

A value-returning function uses return expression;. A void function completes with return; or by reaching the end of its body.

void Reset(int* value)
{
    *value = 0;
    return;
}

Readonly return access

A qualifier before the return type belongs to that type or access path. For example, this free function returns a pointer through which the caller cannot modify the pointed-to integer:

readonly int* GetPointer(int* value)
{
    return value;
}

The caller may store the result in readonly int* pointer and reassign that local pointer, but *pointer = 10; is a compile error. A return value has no binding-level readonly qualifier.

A leading qualifier with no pointer or reference access to restrict is a compile error: readonly int Get(), readonly SomeStruct Get() and readonly int[] Get() are invalid. Remove the qualifier for a by-value result, or move it before the function or method name to declare a readonly callable. Returning readonly int& remains valid because it restricts access through the reference.

A readonly immediately before a callable's name is instead the readonly function/method qualifier: int* readonly GetPointer(int* value) returns an ordinary int* and declares a readonly contract. Free functions and static methods support this qualifier too.

Readonly free functions

A readonly function has no hidden side effects. It may change locals and write through explicitly mutable pointer/reference parameters, while readonly pointer/reference parameters remain protected. The signature shows which external state it may modify.

Readonly functions do not make their inputs readonly

If a function must not write through an incoming pointer, declare that parameter as readonly T* input. An ordinary T* input remains writable inside a readonly function. Use readonly T& for the equivalent restriction on a reference parameter.

int readonly Square(int value)
{
    return value * value;
}

void readonly Process(int* output, readonly int* input)
{
    *output = Square(*input); // allowed: output is explicitly mutable
    // *input = 42;  error: readonly pointee
}

void readonly Increment(int& value)
{
    value++;
}

To also prevent reassignment of the pointer parameter itself, use readonly T* readonly input. In contrast, T* readonly input fixes only the local pointer binding and still permits *input = value;. These restrictions protect access through that parameter, not the underlying memory from changes through other mutable aliases. See the four pointer forms.

Local structs and nested fields remain mutable, and explicit mutable capabilities keep their permissions through local aliases and containers. Copying a pointer from this or static state into a local struct does not grant permission to write through it. Local allocation and cleanup are permitted, including free through an explicit mutable pointer parameter.

Free/static calls inside readonly code must target readonly declarations. A non-readonly instance method is allowed on a local or explicitly mutable receiver only if contextual analysis of its body finds no forbidden hidden effects. Constructors, destructors and property/indexer accessors likewise use the actual receiver and argument origins. Type-level readonly access is never bypassed. An ordinary function or extern declaration is effectful by default, even when its parameters are readonly. Reads of static state are allowed, so readonly does not promise mathematical purity or a result determined only by its arguments. See the complete effect rules.

Function values and lambdas

function R(Args...) is a first-class Xenon callable. It stores an invoke function and, when needed, a closure environment. The distinct function R(Args...)* type is a raw native function pointer containing only a code address.

function int(int) managed;
function int(int)* native;

function int(int) MakeMultiplier(int factor)
{
    return [factor](int value) => { return factor * value; };
}

function int(int) multiply = MakeMultiplier(10);
int result = multiply(5); // 50

Function values work as locals, parameters, returns, fields, generic arguments and array elements. Assigning an ordinary function or a non-capturing lambda creates a function value without allocating an environment:

int AddOne(int value) { return value + 1; }

function int(int) first = AddOne;
function int(int) second = [](int value) => { return value * 2; };

Lambdas use typed parameters and a block body. Return and parameter types come from the contextual function type, while the parameter declarations still state their types. Expression-bodied and lambda-level generic syntax are not supported.

When a lambda is passed to an overloaded call, each candidate supplies its own contextual function signature. Explicit parameter types eliminate incompatible candidates. A capturing lambda also eliminates raw function-pointer candidates. For an otherwise identical non-capturing lambda, the first-class function-value conversion ranks ahead of conversion to a raw function pointer; genuinely equal candidates produce an ambiguity diagnostic.

Every reachable return in the lambda body also participates in candidate selection. A candidate is discarded when a returned expression cannot use the ordinary implicit conversion to that candidate's return type; exact return conversions rank ahead of more costly conversions. These checks are speculative: diagnostics, captures and generated lambda bodies are committed only for the selected candidate. The same rules select among multiple operator implicit(function ...) declarations when a lambda initializes a wrapper value.

Contextual function conversion applies in function and method calls, constructor and base-constructor arguments, indexer arguments and setters, field or local initialization, assignment and returns. Bare, namespace-qualified and static named functions can also flow directly through one user-defined conversion whose input is a matching function value, so an intermediate function local is unnecessary. An implicit operator may explicitly accept a raw function pointer; that route ranks after the function-value route, while direct raw-pointer assignment still uses &FunctionName. If overload or conversion resolution fails, tentative lambda captures do not commit moves, borrows or generated closure symbols. Compound indexer arguments must be copyable because the getter and setter each receive one value while the argument expression is evaluated only once.

Explicit captures

Outer locals and parameters are never captured implicitly. List every captured value and its ownership mode before the parameter list:

CaptureMeaning
[value]Copy by ordinary value-initialization rules.
[move value]Move into the environment and invalidate the source.
[&value]Store a mutable borrow of the original location.
[readonly &value]Store a readonly borrow of the original location.
int total = 0;
{
    function void(int) add = [&total](int value) => {
        total += value;
    };
    add(5);
}

unique<Resource> resource = new Resource();
function void() owner = [move resource]() => {
    resource->Use();
};

A plain value capture uses the same copy rules as assignment. Capturing shared<T> retains shared ownership. Because unique<T> is not copyable, it requires [move value]. Mutable and readonly borrow captures participate in the normal alias and escape analysis; a borrowed closure cannot be returned or stored where it could outlive its source.

Lifetime and copying

Capturing function values share one reference-counted environment. Copying the function value retains that environment, including mutable state and move-owned captures. Moving transfers the function value without retaining it. The last owning function value destroys captures in reverse order and releases the environment deterministically. If capture construction or a call throws, normal Xenon unwind cleanup applies.

Default-initialized function storage contains an empty value with null invoke and control pointers. Empty values may be copied, moved, replaced and destroyed. Calling one while runtime checks are enabled terminates through the standard runtime trap and cannot be caught as a Xenon exception. Builds with runtime checks disabled do not emit this guard.

Raw callback pointers

A non-capturing lambda can still target a raw pointer for C callbacks: function int(int)* callback = [](int value) => { return value * 2; };. A capturing lambda cannot convert to a raw pointer because the raw ABI has nowhere to store its environment. First-class function values are Xenon-only and cannot appear directly in extern or export signatures.

The earlier explicit form function int(int value) { return value + 1; } remains available for capture-free raw function pointers.

Native imports and exports

extern declares a native function implemented elsewhere and ends with a semicolon. export compiles a Xenon body and exposes it under a C-compatible native name. These modifiers apply to namespace-level functions.

extern int puts(readonly byte* text);
export int Add(int a, int b) { return a + b; }

See extern, export and C ABI for symbol naming, the difference from public, library configuration, supported signatures and examples of native consumers.

Program entry point

An executable starts at the namespace-level function int Main(). The compiler creates the native platform entry wrapper and returns the Xenon result to the operating system.

namespace Example;

int Main()
{
    return 0;
}

Resumable functions

The explicit async modifier selects resumable completion semantics. Await is permitted only inside async functions; async functions without await complete synchronously without a coroutine frame. Its library return handle is constructed before execution starts; return statements complete that handle through operator resolve. See Await and resumable functions for the protocol, lifetime rules and diagnostics.