Silk

Lifetimes and elision

A lifetime describes how long a borrowed value is valid. It is part of the static type contract; it is not a runtime token, an allocation, or the identity of a particular owner. Concrete loans separately track which places supply the value and which uses keep those places borrowed.

LIFE-001 — Lifetime parameters name validity relationships

Status: Confirmed

A lifetime parameter begins with an apostrophe. Declarations and nominal applications may include lifetimes and ordinary generic parameters in the same angle-bracket list. Their argument namespaces are separate, so omitting a lifetime does not consume an ordinary type-argument position.

struct View<'data, T> { value: &'data T }

fn wrap<'data, T>(value: &'data T) -> View<'data, T> {
  return View<'data, T> { value: value }
}

References use &'data T or &'data mut T; slices use &'data [T] or &'data mut [T]. string<'text> is an immutable UTF-8 view valid for 'text. Ordinary text and byte literals carry 'static, the distinguished program-valid lifetime. An owned value satisfying a 'static contents bound may still be dropped normally; that bound does not require its owner to live forever.

Boundary: Shared and exclusive views may appear in fields, generic arguments, fixed arrays, and ordinary unions. Each containing type retains their lifetimes. A reference into a local owner's inline storage cannot become 'static, and moving an owner together with such a reference does not make a valid self-referential value. Exclusive fields remain affine. A dependent user Drop keeps all observable borrowed components valid through cleanup, even without an explicit source read. Effect successes and failures preserve these same lifetimes through run, propagation and recovery.

Diagnostics: An unknown lifetime reports SEM0209. Duplicate, invalid, or unsupported binders report the corresponding declaration diagnostic, including SEM0211 for invalid lifetime forms. A use beyond a referent's validity reports OWN0019, including through generic Effect outcomes.

Evidence: lifetime requirements, lifetime syntax tests, lifetime checking tests.

LIFE-002 — Outlives bounds constrain contents and access

Status: Confirmed

'long: 'short requires 'long to cover 'short. T: 'use requires the borrowed contents of T to remain valid for 'use. Multiple bounds use +. The type &'use T itself implies T: 'use.

fn shorten<'long: 'short, 'short>(value: &'long i32) -> &'short i32 {
  return value
}

Shared references are covariant in their lifetime and payload. Exclusive references may shorten outer access, but their target type is invariant: &mut Holder<'long> cannot become a destination for an unrelated Holder<'short>. Nominal lifetime variance follows its fields; opaque or interior mutable storage retains conservative invariant boundaries. Callable inputs reverse variance and results preserve it.

Boundary: A call checks the contract of the operation already selected. Lifetime failure does not choose another implementation, conversion, or provider. A bound constrains validity; it never makes two source owners the same owner. Mutation preserves the destination's declared type.

Diagnostics: An unsatisfied lifetime relation reports SEM0212; an unsatisfied type-contents bound reports SEM0213. The diagnostic identifies the declared requirement and offending source boundary.

Evidence: outlives requirements, compatibility tests.

LIFE-003 — Elision is determined by the declaration header

Status: Confirmed

An omitted lifetime on each independent borrowed input introduces a fresh declaration binder. An outer borrowed receiver supplies omitted result lifetimes; without that receiver, exactly one top-level borrowed input supplies the default. A string input participates as a borrowed view.

fn first(values: &[i32]) -> &i32 {
  return &values[0]
}

Its expanded contract is fn first<'life0>(values: &'life0 [i32]) -> &'life0 i32. Each omitted lifetime in a nominal input introduces an independent binder. Borrowing a wrapper adds its own outer lifetime; that outer borrow does not merge the wrapper's stored-data lifetimes. Omitted nominal output lifetimes use the same result default. Each omitted borrowed field introduces an independent parameter of its containing declaration.

An inherent impl also introduces owner binders for omitted nominal lifetimes. For example, impl<A> SliceStream<A> can define make(slice: &[A]) -> SliceStream<A>: its returned holder uses the input's lifetime, so SliceStream.make(&values) is callable. Returning Self instead keeps the impl's applied owner, including its stored-data lifetimes. It does not connect those lifetimes to an independently elided input; name the shared lifetime explicitly when a constructor returns Self.

Conformance headers apply the same nominal lifetime elision. For a holder with one stored-data lifetime, impl<A: Copy> Stream<A, never ? never> for SliceStream<A> introduces an independent impl lifetime and retains it in the operation contracts. It has the same relationships as an explicitly bound impl<'data, A: Copy> ... for SliceStream<'data, A>. Ordinary type arguments, bounds and conformance selection are unchanged; Self still denotes that fixed applied owner.

Local type annotations instead infer body-scoped lifetimes from their uses and cleanup. Public relationships never depend on return bodies, setter histories, or which constructor happened to initialize a field.

Boundary: Two independent borrowed inputs with no receiver default require an explicit result relationship. A by-value holder's stored view can be returned by naming its data lifetime; elision does not reinterpret that stored view as an outer borrowed receiver.

Diagnostics: Missing or ambiguous output relationships report SEM0210. Editor hover shows stable readable generated binders. Make lifetimes explicit offers a compiler-owned edit only for a complete supported header, preserves comments and semantics, and disables stale actions. Ordinary formatting preserves the author's explicit or omitted choices.

Evidence: elision requirements, declaration tests, editor action requirements.

LIFE-004 — Invocation lifetimes and retained environments are independent

Status: Confirmed

One outer for<'call> may quantify a callable contract. Each invocation must satisfy that contract for a fresh lifetime. fn<'env>(...) -> ... separately names how long the callable's retained captures remain valid; mut and once still express invocation access. A quantified signature may refer to surrounding lifetime and type parameters.

fn identity<'call>(value: &'call i32) -> &'call i32 { return value }

fn apply<'data>(
  callback: for<'call> fn<'static>(&'call i32) -> &'call i32,
  value: &'data i32,
) -> &'data i32 {
  return callback(value)
}

An effect-function declaration may name its retained environment with effect<'env> fn retain<T: 'env, 'env>(value: T) -> i32. The environment resolves against the complete declaration binder list. Naming it retains every obligation of captured contents; it does not make an invalid capture valid.

Effect<'env; A ! E ? R> names an Effect environment's validity independently of success A, failure E, requirement row R, and run access. Omitted environments are elaborated from their header or local context. An Effect that retains borrowed data is not detached merely because its success and failure types contain no views.

Effect environment positions also accept finite intersections: Effect<'call & 'env; A ! E ? R> and effect<'a & 'b> fn. An intersection is valid only while every constituent is valid. It is associative, commutative, and idempotent; 'static is its identity. Canonical identity does not depend on ambient outlives assumptions.

Each constituent outlives the intersection. The intersection outlives another region when every constituent outlives that region. It cannot be promoted to a longer constituent without proof. For example, for<'call> once fn<'env>(&'call mut Resource) -> once Effect<'call & 'env; A ! E ? R> retains both a fresh resource loan and captured data. Anonymous effect callables derive this intersection from their capture validity and retained inputs; generic contents keep their own outlives obligations. The intersection introduces no additional binder or runtime field.

Effect.useReleaseNonParking uses this result contract for both callbacks. It disposes the use Effect and ends its loan before creating the release loan; neither callback may export the resource loan as an outcome. Release remains non-parking and runs during cancellation with the original outcome preserved.

Boundary: Nested quantified callable signatures and unconstrained higher-rank inference are unsupported. A callback cannot store a fresh invocation borrow into longer-lived surrounding storage. An outcome may outlive its computation when it borrows independently valid external data; it cannot borrow storage destroyed with the computation. Suspended partial owners retain their initialized remainder and conditional cleanup flags.

Diagnostics: Unsupported quantifier structure reports SEM0211. Callable incompatibility reports SEM0076; invalid retention uses the ordinary lifetime or ownership diagnostic.

Evidence: quantified callable requirements, lifetime checking tests.

LIFE-005 — Dependent storage keeps cleanup and access lifetimes distinct

Status: Confirmed

RawBuffer<T> is invariant in its payload type. Slot<'storage, T> adds the exclusive storage access lifetime; that lifetime may shorten, while T remains invariant. The elided spelling Slot<T> follows ordinary nominal lifetime elision. A slot cannot escape the buffer that supplied it. Slot projection checks the recorded element count; the caller still proves that the underlying allocation fits that count. Taking an initialized element transfers T, including its external referent lifetimes; copying one requires T: Copy and retains those same lifetimes. Neither operation extends a reference into the buffer's own allocation beyond that allocation's lifetime.

Vector<T> uses these operations in ordinary Silk source. Its empty constructor, ordinary replacement and extraction operations, and insertion Effects with unit outcomes admit shared references and affine elements containing exclusive references. Extracted external references may survive the Vector while their backing owners remain valid. A Vector's Drop cleans its initialized elements before releasing storage, including after growth failure.

A user Drop hook conservatively retains every component its declared receiver could observe. An empty hook body does not waive this contract. Recursive field cleanup follows the initialized remainder: a moved whole Drop-bearing field belongs to its new owner, while its plain former container cleans the fields it still owns. Moving through a whole-value Drop ancestor is invalid. Replacement preserves the declared destination type and evaluates incoming expressions before committing installation. A typed failure cleans the actual remaining state, including earlier permitted moves, without rollback.

Boundary: Raw storage callers still prove bounds, initializedness and aliasing. Hooks remain synchronous, infallible, non-allocating, requirement-free and non-escaping. Suspension preserves partial state; cancellation cleans only initialized components. Fatal traps do not unwind source cleanup.

Diagnostics: Conflicting access reports OWN0010 or OWN0011; invalidation beyond validity reports OWN0019. Invalid replacement uses the ordinary type and lifetime diagnostics.

Evidence: owned-allocation requirements, dependent ownership tests, Vector tests.

On this page