Contract Dispatch¶
Accepted
Accepted for V1 static contract dispatch, opaque static returns, dynamic contract objects, dyn safety, and owned dynamic values.
Catalyst supports static contract dispatch by default and explicit dynamic dispatch through dyn contract objects. This document records the parameter, return, dynamic object, and dyn-safety model.
Static Constraint Parameters¶
The canonical static form uses an explicit comptime type parameter constrained by a semantic contract or structural shape:
fn sum(comptime S: Sequence(f32), xs: *const S) f32
This is always valid and fully names the concrete implementing type parameter.
For ergonomic anonymous static dispatch, impl may appear in pointer pointee position in function parameter declarations:
fn sum(xs: *const impl Sequence(f32)) f32
This is an anonymous static generic parameter form. It means "a pointer to some compile-time-known concrete type implementing Sequence(f32)." The source signature still has one parameter named xs; there is no second source-visible type parameter. The compiler may internally specialize the function body for each concrete argument type as needed, so dispatch remains monomorphized. It does not form a dynamic contract object and does not use a vtable.
Static contract dispatch is a semantic guarantee, not an optimizer promise. V1 guarantees that no dynamic vtable is formed for static constraint parameters and that the concrete implementer type is known for layout and operation resolution; it does not promise inlining quality, code-size behavior, or a specific specialization strategy.
The impl modifier belongs to the pointee type expression, not to the pointer value. The pointer value and pointee value are runtime values. The concrete pointee type is statically known for dispatch and specialization:
*const impl Sequence(f32) // readonly pointer to a static implementer
*const dyn Sequence(f32) // readonly pointer to an erased dynamic object
The same anonymous static form works for structural constraints:
const ParseCapabilities = satisfies(.{
alloc: *dyn Allocator,
diag: *Diagnostics,
})
fn parse({ alloc, diag }: *const impl ParseCapabilities, source: Source) Ast!ParseError
*const impl Sequence(f32) and *const impl ParseCapabilities are not standalone concrete types. In V1 they are allowed only in function declaration parameter position as anonymous static constraint parameters. They have the same dispatch power as explicit comptime S: Constraint forms, except that the concrete type parameter is anonymous and cannot be named in the function body.
Each anonymous static constraint parameter introduces an independent concrete type, even when two parameters have identical-looking constraints:
fn dot(a: *const impl Sequence(f32), b: *const impl Sequence(f32)) f32
a and b may have different concrete sequence types.
Constraint-typed runtime values are not allowed in V1. Semantic contract constraints are used through static parameters, borrowed dynamic pointers, or owned boxes:
fn by_name(comptime S: Contract, x: *const S) void
fn anonymous(x: *const impl Contract) void
fn borrowed(x: *const dyn Contract) void
Box(dyn Contract)
Anonymous static impl pointer forms are function declaration parameter syntax only. They are not valid value types:
var xs: *const impl Sequence(f32) = undefined // error: anonymous impl parameter form is parameter-only
Use a named concrete type parameter, an anonymous static impl pointer parameter, or explicit dyn.
Static constraint parameters reject already-erased dyn arguments. Opening dynamic contract objects for static generic use is deferred.
Opaque Static Returns¶
Opaque static contract returns are required for ergonomic iterator APIs in V1. Accepted syntax uses comptime before the returned contract:
fn iter(self: *const Self) comptime Iterator(T)
The semantic rule is:
- the callee chooses the concrete return type
- all return paths must resolve to the same concrete type
- callers know only the declared constraint
- dispatch remains static and monomorphized
- the compiler knows the concrete layout for code generation
- the concrete return type can remain hidden from the source API
If return paths need different concrete implementer types, the function should return a dyn contract object or an explicit sum/union type instead.
Opaque static returns do not define a stable public ABI boundary in V1. The compiler must know the concrete return layout wherever it generates calls; exported ABI-stable opaque-return functions and separate-compilation guarantees for hidden return layouts are deferred. APIs that need stable erased ABI should return a dyn object or an explicit runtime representation.
Dynamic Contract Objects¶
Dynamic dispatch is explicit with dyn as a type modifier on a dyn-safe contract application. A contract application such as Sequence(f32) is a constraint type. dyn Sequence(f32) is a concrete unsized erased type when the applied contract surface is dyn-safe.
fn sum(xs: *const impl Sequence(f32)) f32 // static specialization
*const dyn Sequence(f32) // borrowed runtime existential, if dyn-safe
Anonymous impl pointers remain invalid outside function declaration parameters:
var xs: *const impl Sequence(f32) = undefined // error: not a runtime value type
Runtime ABI positions require concrete runtime types. dyn Contract is concrete but unsized, so it cannot be stored directly in locals, fields, arrays, by-value parameters, or return slots. Pointer and owner types around it are sized runtime values. Anonymous static impl pointer parameters such as *const impl Contract are static-only parameter forms and are not runtime ABI types.
Function pointers may still target a concrete instantiation of a static-dispatch function. The function pointer signature names the chosen concrete implementer type, such as *const Buffer, not the anonymous *const impl Contract parameter form.
Dynamic contract objects may be used directly:
fn use(xs: *const dyn SomeDynSafeContract(f32)) void
var xs: *const dyn SomeDynSafeContract(f32) = &value
const Processor = struct {
input: *const dyn SomeDynSafeContract(f32)
}
Borrowed dynamic contract objects use ordinary pointer syntax around the erased payload type:
*const dyn Contract and *dyn Contract are valid forms when the applied contract is dyn-safe. *const dyn Contract is a readonly erased referent and can call dynamic operations whose receiver type is *const Self. *dyn Contract is a mutable erased referent and can also call dynamic operations whose receiver type is *Self.
*dyn C may coerce to *const dyn C under the ordinary pointer constness rule. This changes the erased data pointer from mutable to readonly for the target expression. It does not change the visible contract surface, perform a sidecast, search for another conformance, or rebuild the vtable.
Owned erased contract values use Box(dyn Contract). Box(dyn Contract) is a concrete sized owning resource descriptor. It owns a concrete payload whose type is erased behind the dyn-safe contract surface.
A borrowed dynamic contract pointer is a runtime fat pointer. For a single readonly contract pointer it is conceptually:
{
data: *const opaque,
vtable: *const ContractVTable,
}
For a mutable contract pointer, the erased data pointer is mutable:
{
data: *opaque,
vtable: *const ContractVTable,
}
opaque is the erased pointee type used for dynamic dispatch and low-level erased data pointers. It is not void: void means no value. *opaque and *const opaque can be passed around but cannot be dereferenced without explicit typed recovery. See Reference and Mutability for cast, as_type, and assume_aligned.
For an intersection contract object, it contains one data pointer and one vtable pointer per normalized contract component:
*const dyn (Sequence(f32) & RealtimeSafe)
{
data: *const opaque,
sequence_vtable: *const Sequence_f32_vtable,
realtime_safe_vtable: *const RealtimeSafe_vtable,
}
Concrete pointers implicitly coerce to *dyn Contract or *const dyn Contract when the expected type is dynamic, the conformance is visible, and the applied contract is dyn-safe:
fn use(xs: *const dyn SomeDynSafeContract(f32)) void
var value: Concrete = ...
use(&value)
The source must provide a concrete pointer. For slices, the implementer is the slice descriptor, so &slice is required.
Dyn Safety¶
Contracts may use Self freely for static dispatch. dyn contract objects require dyn-safe applied contracts. V1 computes a dynamic-mode active surface by evaluating operation guards for the dynamic surface; dyn Contract, *dyn Contract, and Box(dyn Contract) are valid only when that computed surface can be represented honestly as a vtable.
An applied contract is dyn-safe when, after substituting contract parameters and evaluating guards, every active operation can be represented as a fixed vtable slot callable through an erased receiver, and no active operation requires moving, returning, constructing, or statically specializing the hidden Self type.
The active surface includes:
- operations declared directly in the contract
- inherited/base contract operations
- default methods
- active guarded operations after guard evaluation
Inactive guarded operations are absent and do not affect dyn safety.
There is no ad hoc or best-effort partial vtable outside the guard model. If an active operation is not dyn-safe, the applied contract cannot form a dynamic object unless the contract author guards that operation out of the dynamic surface.
Initial dyn-safety rules:
- operations must not return bare
Self - operations must not take bare
Selfby value Selfmay appear only in the receiver parameter- dynamic-callable operations must not have operation-level
comptimeparameters - dynamic-callable operations must have a first parameter whose type is
*Selfor*const Self - all parameter and return types must be concrete runtime ABI types after substituting contract parameters
dyn Contractis concrete but unsized only when the applied contract is dyn-safe*dyn Contractand*const dyn Contractcount as concrete sized runtime ABI types only when the applied contract is dyn-safe- anonymous static
implpointer parameters such as*const impl Contractare static-only and are not valid in dyn vtable signatures - anonymous
implpointer forms such as*const impl Sequence(u8)are never runtime ABI types - error types must be fully known; inferred error sets are not valid in dynamic vtable signatures
Dyn safety is checked when forming or naming an applied dyn contract object type, not at contract definition time. A non-dyn-safe contract may still be used for static dispatch.
By-value Self parameters are not dyn-safe even when all known implementers are copyable or small. The concrete layout is erased, and the dynamic vtable slot must be uniform for every visible implementer. Use a receiver pointer such as self: *Self or self: *const Self, or move ownership through an explicit erased owner such as Box(dyn C) outside the vtable receiver position.
self: Box(Self) does not define a receiver in V1. Receiver classification recognizes only Self, *Self, and *const Self, and dyn safety accepts only the pointer receiver forms. A first parameter such as self: Box(Self) is therefore not receiver-callable through dyn; consuming dynamic APIs should take or return an explicit erased owner such as Box(dyn C) outside the receiver model.
Dyn-safety checks the resolved semantic signature. Method-call auto-addressing is call-site sugar and does not create additional vtable receiver shapes.
Valid dyn-safe contract surface:
const Countable = contract {
fn len(self: *const Self) usize
}
Invalid dynamic surface because clone returns the hidden concrete Self type:
const Cloneable = contract {
fn clone(self: *const Self) Self
}
fn use(x: *const dyn Cloneable) void // error: Cloneable is not dyn-safe
Contracts may require receiver and non-receiver operations. Static dispatch supports both. V1 dyn requires every active operation to be receiver-callable, because a dynamic contract object represents one erased concrete value plus a vtable. Non-receiver operations are associated with a conformance but do not consume the erased object, so they make the applied contract non-dyn-safe in V1 unless a future design adds explicit static-only or non-vtable operation annotations.
Default methods are part of the contract method surface and count toward dyn safety. If an active default method is not dyn-safe, the applied contract is not dyn-safe. If it is dyn-safe, the vtable slot points to either the implementation override or a compiler-generated wrapper for the default body specialized to the concrete implementer. Dyn-safe default methods are real dynamic operations, not static-only helper functions.
Dyn vtables contain every active contract method, including required operations and default methods. Vtables are not limited to required operations.
Readonly and mutable erased pointers do not form separate dynamic surfaces. A dyn-safe contract may include both *const Self and *Self receiver operations. *const dyn C values can call only the readonly receiver operations, while *dyn C values can call both readonly and mutable receiver operations. The vtable surface is the same; pointer constness controls call eligibility.
Dynamic vtable slots use implementation operations only after ordinary signature compatibility has accepted them. A readonly contract receiver such as *const Self cannot be implemented by a mutable-only receiver such as *Self, because that slot must be callable from *const dyn C. The existing authority-reducing relaxation goes only the other direction: a contract operation requiring *Self may be implemented with *const Self when the implementation does not need mutation.
Accepted implementation relaxations are compatibility rules, not vtable shape rules. A dynamic vtable slot uses the contract operation's declared parameter and return types after substitution. If an implementation uses pointer/slice const relaxation or returns a narrower error set, the wrapper or slot presents the contract-declared signature so every implementer of the same dynamic surface has the same ABI signature.
Guarded operations are considered only after applying the contract and evaluating the guard. If the guard is false, the operation is absent. If the guard is true, the operation must be dyn-safe or the applied contract is not dyn-safe. An active default method must also be honest for the dynamic surface it remains in; if its body relies on a static-only operation, that call must be excluded from the dynamic specialization by a directly evaluated comptime surface guard, or the default method must be guarded out of the dynamic surface.
This allows a contract to expose different static and dynamic surfaces without making the dynamic object dishonest. For example, Iterable(Item).iter is guarded to the static surface because it returns an opaque static iterator, while iter_dyn remains active in the dynamic surface and can be called through *const dyn Iterable(Item) to produce Box(dyn Iterator(Item))!AllocError.
Static-only required operations may remain part of static conformance checking while being absent from the dynamic vtable. Dyn safety checks only the active dynamic surface after guards are evaluated, but that remaining surface must still satisfy all dyn-safety rules and contain at least one active receiver-callable operation. For contract-only intersections, the non-empty requirement applies to the final normalized dynamic surface as a whole, not to each component independently. V1 does not treat a fully empty dynamic surface as dyn-safe; standalone marker-style dyn objects are deferred.
Contract-level comptime parameters are substituted before dyn-safety is checked. Operation-level comptime parameters are not dyn-safe in V1, even when constrained, because a dynamic vtable slot must describe one concrete runtime ABI function and cannot require call-site specialization. Operations that need method-level comptime specialization must be guarded out of the dynamic surface.
A dynamic operation may use ordinary error returns only when the error type is explicit and fully known after substitution. Inferred error sets, including T! and void! source forms, are not dyn-safe because a dynamic vtable slot needs a stable ABI signature. Operations that need local error inference must be guarded out of the dynamic surface or paired with an explicit-error dynamic operation.
A dynamic operation may mention another erased contract object type such as *const dyn D or Box(dyn D) when D is dyn-safe. These are concrete runtime types whose own hidden payload is erased by their visible contract surface; they are not hidden Self uses. Returning Box(dyn D) is dyn-safe under the same rule because the returned value is a concrete sized owner descriptor. Allocation fallibility is modeled by the operation's ordinary error return.
Only methods declared by the contract participate in dyn method dispatch. External helper functions may accept *dyn Contract parameters like ordinary functions, but they are not vtable members and do not become dynamically dispatched methods. This keeps vtable layout determined solely by the contract definition.
Dynamic operations are selected from the visible erased type. If a value is *dyn Sequence(T), operations inherited from Sequence(T)'s base contracts are visible through that dynamic surface. If a value is *dyn (A & B), operations from visible intersection components are visible. Method-name collisions between visible intersection components do not make the dynamic surface non-dyn-safe; they remain lookup-time ambiguity errors as documented in Contract Dependencies and Intersections. Unqualified dynamic calls do not choose a collided operation by normalization order. The compiler does not search for unrelated contracts that the hidden concrete payload might implement but that are not exposed by the erased type.
Dynamic dependency upcasts and intersection subset upcasts select already represented metadata from the erased surface. They do not re-run hidden concrete conformance lookup in the current scope, allocate, synthesize runtime vtables, or rebuild descriptor state.
Hidden concrete inherent methods are also not searched by dynamic method lookup. If exactly one visible erased operation has a given unqualified name, x.foo() may dispatch to that operation even when the hidden concrete payload has an unrelated inherent foo method.
Qualified contract-call syntax may name a dynamic operation explicitly:
Sequence(T).len(xs)
Iterable(*const T).iter_dyn(xs, alloc)
For a concrete receiver, this is static dispatch through the visible conformance. For a receiver of type *dyn C or *const dyn C, the call is valid only when the named operation is part of the visible erased surface of C, including inherited base operations and visible intersection components. It then dispatches through the dynamic vtable. A qualified call may select a visible intersection component directly; A.foo(x) is valid for x: *dyn (A & B) when A.foo is in the visible erased surface. The receiver does not need to be upcast to *dyn A first. The qualified contract name does not trigger runtime sidecasts or hidden conformance lookup.
Explicit dyn-safety attributes are deferred. Dyn-safety queries should be expressible through comptime reflection or standard-library helpers.
Owned Dynamic Values¶
V1 supports owned dynamic contract values through Box(dyn Contract):
Box(dyn Contract)
Box(dyn Contract) is an ordinary prelude resource type, not compiler magic. The language support required to make it possible is the general dyn Contract unsized type model, vtable metadata, and reflection APIs for creating dynamic dispatch and payload disposal metadata.
Prelude Box forms owned dynamic values by requesting compiler-checked dyn metadata for the concrete implementer and visible contract surface, such as I.dyn_metadata(C). The canonical metadata API is documented in Dynamic Contract Metadata. The canonical owned dynamic box model is documented in Box Resource.
var boxed = try box_dyn(Iterator(i32), RangeIterator, alloc, RangeIterator{ .next_value = 0, .end = 10 })
defer boxed.dispose()
var it = boxed.ptr()
while const item = it.next() {
}
Borrowed dynamic dispatch applies to *dyn C / *const dyn C. Owner behavior, explicit ptr() / ptr_const() access, closed boxed iterator forwarding, box / box_dyn, into_dyn, upcast, dispose metadata, and layout/performance requirements all belong to the Box resource design.