Await and resumable functions

An await expression can suspend a function and return control to its caller. Ordinary library types define awaiting and completion through operators; the explicit async modifier selects completion semantics without a built-in Task type or scheduler.

Await expressions

await is a prefix expression with unary precedence. It binds to its operand before binary operators: await operation + 1 means (await operation) + 1. Parenthesize a larger operand when necessary. Await expressions can appear in arguments, initializers, branches, loops and other expressions.

async Result Load()
{
    int first = await Read();
    int second = Combine(first, await Read());
    return first + second;
}

await is permitted only inside an explicitly async function or lambda. An ordinary function returning a task-like handle remains ordinary: it returns that handle directly. Its return statements never select completion operators. Nested lambdas require their own async modifier.

Awaiting a library value

public static bool operator await(
    readonly Operation& value,
    storage<int>& result,
    function void() continuation)
{
    // If complete: initialize result and return true.
    // Otherwise: retain continuation, leave result empty, return false.
}

The first parameter follows ordinary operator lookup, accessibility, conversion and qualifier rules. It need not be readonly. The return type must be bool; the final parameter must be exactly function void(). A value-producing overload takes writable storage<T>& as its middle parameter and makes the await expression have type T.

The two-parameter form operator await(Operand, function void()) has no result storage and makes the expression have type void. Equally applicable value and void overloads are ambiguous; the surrounding expected type does not choose between them.

Operator outcomeRequired behavior
trueResult storage is initialized. The function moves the value out, leaving storage empty, and continues immediately.
falseStorage is empty. The native call returns; execution resumes only after a continuation notification.
throwThe exception occurs at the original await expression, including on a later retry, and enters its normal exception handlers.

The operand is evaluated once and retained across suspension. Each notification retries the selected operator against that same materialized operand. A by-value operand must be copyable because every retry receives an ordinary copy; a reference operand can await a move-only object. Conversions remain ordinary Xenon conversions. The compiler never default-constructs T to obtain the result: non-default-constructible and move-only results, including unique<T>, use storage.

Returning and completing

An async return type is an ordinary struct with completion operators. A suspending implementation additionally requires a copyable handle so the caller and frame can share its state. It must support normal parameterless construction. If no constructors are declared, existing implicit initialization applies. Once constructors are declared, an accessible zero-parameter constructor is required. There is no synthesized extra constructor.

The wrapper constructs the return object first, copies its handle into the frame, starts execution and returns the caller's handle. The library must make both handles refer to one logical completion state, for example through shared<State>. An inline value field alone does not provide shared completion state.

struct State { public int Status; public int Value; }
struct Result
{
    public shared<State> Data;
    public Result() { Data = new State(); }
    public static void operator resolve(Result& target, int value)
    { target.Data->Value = value; target.Data->Status = 1; }
    public static void operator reject(Result& target, int error)
    { target.Data->Value = error; target.Data->Status = 2; }
}

Inside a resumable function, return value; selects operator resolve(ReturnType&, ValueType). return; and reaching the end select operator resolve(ReturnType&). Both forms return void and require a writable target reference. Normal functions retain their existing return semantics.

An exception that escapes the body selects a typed operator reject(ReturnType&, ErrorType), returning void. There is no required Exception base class. Ordinary catches, inheritance and overload resolution apply. The compiler checks known escaping exception types through source and imported function bodies, including cleanup. No reject overload is required when existing exception analysis proves that no exception can escape the body. A missing overload names the escaping exception type.

Return arguments are evaluated and kept alive while lexical cleanup and finalizers run. A cleanup failure enters exception handling and rejection before successful completion is published. The generated function invokes one terminal completion operation. If that operation or final frame-handle destruction throws, execution terminates; it never tries a second completion or sends the exception into the continuation caller. Explicit library calls must define their own policy for already-completed objects.

resolve(result, 42);
resolve(voidResult);
reject(result, error);

These contextual operations use operator resolution. No methods named Resolve, SetResult, Reject or GetResult are searched.

Continuation and threading contract

A continuation carries no value, exception or status. It only requests another attempt. The awaitable must retain the supplied function value before returning false and publish its state before invoking it. Ordinary async calls run immediately up to suspension. The compiler supplies no worker pool or scheduler. Async Main has the root continuation pump described below.

Invoking a continuation before the operator returns is supported. An atomic running/pending guard serializes execution and processes the request after the active invocation yields. Continuations can run on another thread; the thread that wins the resume request executes the next segment. There is no automatic thread or synchronization-context affinity.

Each registration is a one-shot notification. Duplicate calls and notifications from older registrations are ignored. If a retry still returns false, the operator must retain the newly supplied continuation. Old callbacks remain safe after terminal completion: they retain a small control record, while the completed execution frame is released.

Frame serialization does not synchronize arbitrary library fields. An awaitable that is completed from another thread must use appropriate atomics or locks for its own data, and safely publish its continuation. Keeping a pending operation alive and eventually notifying it is the library's responsibility. Dropping a return handle does not cancel the function. There is no implicit cancellation protocol.

Lifetimes and exception regions

Live values are retained in a resumable frame. Existing move state, partially moved fields, storage initialization state and destruction responsibility survive each suspension. Temporaries live to their normal full-expression boundary; they are not destroyed merely because an await returned false.

async Result Use()
{
    Resource owner = Resource();
    Resource& reference = owner;
    try
    {
        int value = await Read();
        return value + reference.Value;
    }
    catch (readonly int& error)
    {
        return error;
    }
}

Each suspension point has its own liveness analysis, including later reads, loops, exceptional exits, finalizers and destruction obligations. A reference parameter or borrowed receiver used only before suspension is accepted. A local borrow that is no longer used does not become a frame field merely because its lexical scope contains await.

The generated frame uses the existing pin<Frame> semantics. It is constructed at its final address; TypeFacts.CanRelocate, move and copy rules prohibit relocation for its whole lifetime. An owning local and a reference into it can therefore survive together. A borrow into retained unique/shared ownership uses the existing ownership proof. Raw pointers remain raw pointers and carry their normal explicit lifetime responsibility.

async Result Use(Resource& input)
{
    int saved = input.Value; // input is dead before suspension
    Resource owner = Resource(saved);
    Resource& reference = owner;
    int[] buffer = int[128];
    int[]& view = buffer;
    view[0] = 42;
    await Read();
    return reference.Value + view[0];
}

Use Xenon's array declaration syntax int[] buffer = int[128];; the allocation size belongs to the initializer. int[]& denotes a reference to the array descriptor. A fixed-size local allocation live across await stores its actual backing bytes inline in the frame with normal alignment and element cleanup. There is no separate hidden heap array and no copied external array. Arrays that finish their use and cleanup before suspension can remain native-stack allocations, including runtime-sized arrays. A runtime-sized allocation that actually survives await is diagnosed because it has no fixed inline frame layout.

A resumable result automatically inherits lifetime constraints from external borrows that are live across suspension points. For example, async Result Read(Resource& resource) has an inferred constraint lifetime(result) <= lifetime(resource) when it uses the resource after await. A parameter or receiver used only before suspension contributes no dependency. Multiple suspension points contribute the union of their external dependencies. Local owners and their self-borrows remain owned by the pinned frame.

async Result Read(Resource& resource, int[]& values)
{
    await WaitForInput();
    return resource.Value + values[0];
}
async Result Caller()
{
    Resource resource = Resource(20);
    int[] values = int[16];
    values[0] = 22;
    Result operation = Read(resource, values);
    return await operation;
}

The result type must supply the ordinary await protocol to be awaited by the caller. A reference to an array descriptor preserves the lifetime of its original backing storage; no external array or object is copied. A resumable instance method that retains this constrains its result to the receiver's lifetime. Borrowed captures follow the same rule, while owned captures do not add a dependency on their former owner.

Dependencies belong to values and survive copy, move, assignments, borrowed aggregate fields, forwarding functions and generic specialization. Returning Read(local, array) from an ordinary function is rejected if either referent dies at that return. Writing a dependent result to an outer local, static field or external object is likewise rejected when that destination can outlive a referent. Reference aliases preserve the original referent's provenance. A forwarding function can export dependencies on its own parameters or receiver.

Destroying a result handle does not cancel its coroutine. A scoped operation borrowing a local owner must therefore be awaited on every exit before the owner is destroyed or moved. Copies share the same completion obligation: awaiting a copy satisfies it. Different operation fields retain distinct obligations. A new resumable call creates a separate operation; completing it is not automatically taken as proof that an operation retained in its arguments has completed. Discarding a dependent operation inside a helper cannot hide its escape. This check is conservative; manually signaling library state is not recognized as proof of completion. Static referents do not require a scoped completion obligation.

A borrowed await result retains the provenance of its awaitable. Owning scalar results do not acquire a lifetime dependency merely because they were read through a reference. Raw pointers retain their existing explicit lifetime responsibility.

Owned lambda captures, including move-only values, survive through a retained reference to their existing owning closure environment. They are not copied into a second environment. Borrowed captures used only before suspension are accepted; captures that remain live require the same referent proof as other borrows. Cleanup follows source scopes, storage state and partial moves; values destroyed before a later await do not remain live for that suspension.

Await inside a protected try body works with ordinary catches and finalizers. Suspension inside catch or finally is diagnosed because the active exception context is thread-local. This restriction does not prevent their ordinary cleanup from surrounding a suspended try body.

Async completion without suspension

async Task<int> GetValue()
{
    return 42;
}

Task is only an example library name. Any suitable Future, AppResult or other type works. The compiler constructs the return object, runs the body and calls resolve(result, 42). Without await it uses synchronous completion without a coroutine frame or continuation. No warning is produced. An await in unreachable source may conservatively retain state-machine lowering.

Empty bodies and implicit fallthrough require resolve(Result&). A value-only completion protocol must produce a value on every path; no default logical value is synthesized. Escaping body exceptions become reject, after ordinary local catches and finalizers. A failure constructing the return object occurs before the async body starts.

The modifier can accompany existing access, static and dispatch modifiers. Constructors, destructors, fields, properties, operators and extern functions cannot be async. Inline is not a Xenon modifier. Async does not change function types or dispatch signatures: an async override can implement an ordinary declaration with the same return type. Lambdas accept async function Result() { ... } or a contextual async [captures]() => { ... }.

Async entry point

async AppResult Main()
{
    int value = await Read();
    return value;
}

The native entry bridge calls source Main and observes its result through the public operator await protocol. A logical int becomes the process exit code; a void-like await completion returns 0. Rejection must be reported by the await operator as an exception and follows the usual unhandled entry exception policy (reporting the exception and exiting with code 1). Main remains parameterless, matching the existing int Main() entry rule.

The bridge retries await after notification and keeps the process alive during suspension. Notifications may be direct, reentrant or arrive from another thread. The entry bridge waits without busy spinning. The awaitable or its library owns scheduling and invokes the supplied continuation when its state changes. The compiler does not run third-party event loops or provide a callback queue. Captured continuations keep their notification state alive; late notifications after completion are harmless.

Generics, libraries and tools

Operators on generic structs are specialized through ordinary Xenon generic machinery. Source-free .xelib images preserve stable await, resolve and reject operator identities, the IsAsync flag, result types, completion bodies and suspension nodes. Inferred result lifetime contracts and store effects are exported as semantic metadata, including when ordinary bodies are omitted on import. Their native operator names use op_await, op_resolve and op_reject.

LLVM lowers the bound suspension nodes into resumable entry, resume and cleanup functions, including in debug builds. The language server and VS Code extension recognize await expressions and completion operations and provide keyword completion, highlighting and operator snippets. No standard-library Task implementation is required.