Requires Clauses

Requires Clauses

requires attaches compile-time constraints to declarations. A requires clause specifies a condition that must hold at compile time if it doesn't, the program does not compile. There is no runtime fallback, no dispatch, no branching. requires is a static gate.

For runtime-conditional dispatch, see Where Clauses.

PropertyGuarantee
Evaluation timeCompile time, always
Memory safeYes
Runtime costZero
Valid on functionsYes
Valid on typesYes
Valid on interface methodsYes
Valid on interfacesYes (constrains conforming types)

Basic Syntax

A requires clause appears after the parameter list and return type, before the function body:

fn divide(a: i32, b: i32) -> i32
  requires b != 0 {
    return a / b
}

If the condition cannot be evaluated at compile time, the compiler emits a hard error:

error: requires expression is not compile-time evaluable
  --> src/math.k:1:32
  1 | fn divide(a: i32, b: i32) -> i32
  2 |   requires b != 0 {
    |            ^^^^^^ depends on runtime value 'b'
  note: for runtime-conditional dispatch, use 'where'

Multiple conditions are combined with boolean operators in a single expression:

fn safe_index(arr: [i32], i: i32) -> i32
  requires i >= 0 && i < arr.length() {
    return arr[i]
}

Compile-Time Expressions

Any expression the compiler can evaluate statically is valid in a requires clause:

ExpressionExample
sizeof Trequires sizeof T <= 64
alignof Trequires alignof T >= 16
Arithmetic and comparisonsrequires N > 0 && N <= 1024
eval variables and functionsrequires is_power_of_two(N)
Type trait checksrequires T impl Comparable
typeof queriesrequires typeof T == i32

If the expression depends on a runtime value, it belongs in a where clause, not requires.


Type Constraints

impl and derives bounds can appear in requires clauses as an alternative to inline bounds on type parameters. The compiler desugars them identically:

// These are equivalent:
fn <T impl Serializable> save(data: T) { ... }
fn <T> save(data: T) requires T impl Serializable { ... }

Either way, the bounds are all a generic body can rely on. The body is checked once, where it is defined, against its bounds, never per instantiation. Calling a method no bound declares is a compile error in the generic itself, even if every type it is ever instantiated with happens to have that method. See Type parameter constraints and Generic Bodies.

The requires form is preferred when constraints are long or involve relationships between multiple type parameters:

fn <T, U> convert(input: T) -> U
  requires T impl Serializable && U impl Deserializable {
    var bytes = input.serialize()
    return U::from_bytes(bytes)
}

Requires on Types

requires on a class, struct, or enum is checked at instantiation time. A violation is a hard compile error at the point of use:

class <T> SmallBuffer
  requires sizeof T <= 128 {
    var data: [T; 16]
}

SmallBuffer<i32>()         // ok: sizeof i32 == 4
SmallBuffer<[u8; 256]>()   // compile error: sizeof [u8; 256] == 256, exceeds 128

struct <T> Aligned16
  requires alignof T >= 16 {
    var value: T
}

enum <T> Compact
  requires sizeof T <= 8 {
    Some { value: T },
    None,
}

Type-level constraints are always requires. There is no runtime dispatch on type instantiation either the type is valid or it isn't.

Partial specializations selected by requires

A partial specialization may carry a requires clause: it is chosen for an instantiation only when its pattern matches and its clause holds there. Its pattern can be the type's own parameters:

struct <T> Store { var value: T }
struct <P> Store<P> requires sizeof P <= 8 { var small: P }   // Store<i32> is this one
struct <P> Store<*P> requires sizeof P >= 8 { var wide: *P }  // Store<*i64> is this one

The more specialized pattern wins first (Store<*P> over Store<P>); between two equal patterns, one with a clause wins over one without, and two with clauses that both hold are ambiguous (an error). The clause is decided at compile time from literals, sizeof and alignof of builtin types, pointers and arrays of them, arithmetic and comparisons. A clause the compiler cannot decide for an instantiation (the sizeof of a struct, an eval function) is an error where that instantiation is used, and a conformance (requires P impl I) in a partial's clause is not supported yet.


Requires on Interface Methods

requires on an interface method constrains what conforming types must satisfy. The conforming type must implement the method with an identical requires clause:

interface Accumulator {
    fn add(self, value: i32) -> Self
      requires value > 0
}

class Counter impl Accumulator {
    var total: i32

    fn Counter(self) { self.total = 0 }

    fn add(self, value: i32) -> Self
      requires value > 0 {
        self.total += value
        return self
    }
}

Because interfaces are zero-cost structural contracts, requires is the only valid constraint keyword on interface members. where is not permitted runtime dispatch inside an interface method would violate the zero-cost guarantee.


Requires on Interfaces

A requires clause on an interface declaration constrains which types can conform. A type that does not satisfy the requires cannot implement the interface, even if it has all the required methods:

interface <T> SmallStorable
  requires sizeof T <= 64 {
    fn store(self) -> [byte]
    fn load(data: [byte]) -> T
}

This is distinct from requires on an individual method it applies to the entire interface as a precondition on conformance.


Requires and eval

requires expressions are evaluated by the same compile-time engine as eval. Any eval function or variable is usable in a requires clause:

eval fn is_power_of_two(n: i32) -> bool {
    return n > 0 && (n & (n - 1)) == 0
}

fn <eval N: i32> clear_aligned(@inout buf: [u8; N])
  requires is_power_of_two(N) {
    for i in 0..N { buf[i] = 0 }
}

var a: [u8; 256]
var b: [u8; 300]
clear_aligned<256>(&a)  // ok: 256 is a power of two
clear_aligned<300>(&b)  // compile error: 300 is not a power of two

See Eval for compile-time evaluation rules.


Requires and panic

requires and panic serve different purposes and can coexist. requires filters invalid inputs before the function body executes. panic handles valid inputs that produce errors during processing:

struct PortRange {
    var port: i32
    fn op panic (const self) -> string { return f"port {self.port} out of range" }
}

fn parse_port(input: string) panic -> i32
  requires input.length() > 0 {
    var port = std::parse<i32>(input)
    if port < 0 || port > 65535 {
        panic PortRange { port: port }
    }
    return port
}

The requires clause rejects empty strings at compile time (when the argument is a constant) or documents the precondition for the caller. The panic handles the runtime error case of a non-empty string that isn't a valid port.


Summary

// Compile-time size constraint on a generic
fn <T> stack_alloc() -> T
  requires sizeof T <= 4096 {
    // ...
}

// Type constraint in requires clause
fn <T, U> transform(input: T) -> U
  requires T impl Readable && U impl Writable {
    // ...
}

// Requires on a class
class <T> InlineBuffer
  requires sizeof T <= 64 {
    var data: [T; 8]
}

// Requires on an interface method
interface Validator {
    fn validate(self, input: string) -> bool
      requires input.length() > 0
}

// Requires with eval function
eval fn fits_in_cache_line(n: i32) -> bool = n <= 64

struct <T> CacheAligned
  requires fits_in_cache_line(sizeof T) {
    var value: T
}

See also