Skip to content

Function Shape and Capabilities

Accepted

Complete for V1 source semantics; capability ordering is a lintable convention, not a semantic category.

Function Shape

Canonical full-language shape:

fn read({ fs, alloc }: *const IoContext, path: Path) []u8!ReadError
{
}

Conceptually:

fn name(params) ReturnType!ErrorType
{
}

Compile-time-only function declarations add comptime before fn:

comptime fn validate_len(n: usize) void {
}

comptime fn is a callability restriction. Calls to such functions are valid only during comptime evaluation, and the call itself implicitly forces comptime evaluation. Ordinary functions remain callable at comptime when a forced or demanded comptime context evaluates them with comptime-known inputs. The full execution model is documented in Comptime.

Expression-Bodied Functions

An expression-bodied function uses => expression as the function body:

fn double(x: i32) => x * 2

=> is general function declaration syntax. It is valid on ordinary functions, methods, and comptime fn declarations. It is not limited to type factories.

Rules:

  • the signature before => is the same signature shape used by block-bodied functions.
  • the => body marker must stay on the same logical line as the completed signature.
  • return annotations may be explicit, omitted for inference, or use T! error-set inference according to the normal function inference rules.
  • the expression after => is checked as the function body's only body expression.
  • the function result is produced through normal block statement completion, not by inserting a hidden return.
  • the arrow body may be any expression, including a block expression or a diverging expression such as return expr.
fn increment(x: i32) i32 => x + 1
fn load(path: Path) Module! => parse(path)
fn Buffer(comptime T: Type) => struct {
  data: []T
}

Block expressions are valid arrow bodies:

fn f() => {
  const x = compute()
  x + 1
}

This form is valid but lintable as style/redundant-arrow-block because the block body can usually be written directly.

return remains a normal diverging expression. => return expr is valid where return expr is valid, but lintable as style/redundant-arrow-return.

Declarations are not expressions, so local declarations require a block body or an arrow body whose expression is a block:

fn f() => const x = compute()

Encouraged diagnostics should explain likely fixes for invalid arrow forms when cheap to do so, such as an expected expression after =>, a declaration where an expression is required, or a newline that ended the expression-bodied function before a leading operator.

Capabilities are ordinary parameters. If a function needs one capability, pass it directly:

fn alloc_node(alloc: *impl Allocator, value: Node) *Node!AllocError {
}

Catalyst calls explicit operational authority values capabilities. Capabilities include allocation, diagnostics, IO, filesystem access, logging, clocks, randomness, interning, cancellation, host access, and similar handles.

Parameter Order

Parameter order is a convention, not a language rule:

  • comptime parameters may appear anywhere, but type-shaping comptime parameters conventionally come before capabilities.
  • A receiver remains first for methods and contract operations.
  • Capability parameters conventionally occupy the first non-comptime, non-receiver parameter slots.
  • Domain inputs follow capabilities.
  • Defaulted option parameters come last.

Examples:

fn box(comptime T: Type, alloc: *impl Allocator, value: T) Box(T)!AllocError {
}

fn write(self: *Self, io: *Io, bytes: []const u8, flush: bool = false) usize!WriteError {
}

This order is lintable as a style convention, but V1 gives the compiler no semantic "capability parameter" category. Lints must rely on project configuration, known capability types, names, or structural shapes, and should account for ordinary parameters that happen to use names such as context.

Context Records

When several capabilities travel together, use an ordinary struct as a context record by convention and destructure the fields a function actually uses:

const CompilerContext = struct {
  alloc: *dyn Allocator
  diag: *Diagnostics
  interner: Interner
}

fn parse({ alloc, diag }: *const CompilerContext, source: Source) Ast!ParseError {
}

The destructured parameter is still an ordinary parameter. "Context record" is not a language-level category; the caller passes an ordinary struct value:

var ast = try parse(&ctx, source)

There is no separate needs / use dependency channel in V1, and capabilities are not inferred from function bodies.

Allocator fields in context records follow the direct-vs-erased authority rules documented in Capabilities and Destructuring.

Capability records are passed by pointer by convention. Use *const Context when the function only reads capability fields, and *Context only when it mutates the record's field slots. This is shallow const: a *const CompilerContext can still expose a field such as diag: *Diagnostics.

Structural Capability Constraints

For reusable generic code, structural capability constraints are the canonical context-record shape:

const ParseCapabilities = satisfies(.{
  alloc: *dyn Allocator,
  diag: *Diagnostics,
})

fn parse_any({ alloc, diag }: *const impl ParseCapabilities, source: Source) Ast!ParseError {
}

The anonymous *const impl ParseCapabilities parameter is function declaration parameter shorthand for an implicit concrete context type satisfying the structural constraint. Extra fields on the caller's concrete context are allowed. Field requirements are name-based and use normal assignability to the required field type.

Domain code should still prefer concrete context structs when the parameter represents domain state, cached state, invariants, lifecycle, or a stable ABI. Structural capability constraints are for APIs that only need an open set of capability fields.