Skip to content

6. Value semantics: ownership, copy, move, drop

This is the heart of the language. Everything else is ordinary.

A struct has at most one of each — a constructor, a drop and a copy:

struct Buffer<T> {
Buffer(capacity: u32) { ... } // constructor: name matches the struct, no return type
}
impl<T> Drop for Buffer<T> { // destructor
fn drop(&mut self) { ... }
}
impl<T: Copy> Copy for Buffer<T> { // copy constructor, for the instances it covers
fn copy(&mut self, other: &Self) { ... }
}

The constructor is a member, since it is how a value comes to exist. drop and copy are implementations of the standard library’s Drop and Copy traits (§7.6): they are behaviour a type has, and a bound can then ask for it — T: Copy. They are stricter than other implementations, because they become the struct’s C++ destructor and copy constructor and so are part of its definition: written in the struct’s own module, for the struct itself — every instance, impl<T> Drop for Buffer<T>, never one — and impl Drop’s parameters take no bounds, since a destructor cannot exist for some instances and not others. Neither is ever called by name: drop runs at the end of a scope, and copy on every copy. [DECIDED — §18.31]

That is the complete list. There is no move constructor, no move assignment, no copy assignment, no default constructor to write, no rule of five. The compiler synthesises whatever C++ needs from these three (§6.6).

Every value is always initialised. There is no uninitialised memory in Dazzle.

let f: File // fully constructed, fields at their declared defaults
let n: i32 // 0
let p: ^Player // null
let b: Buffer<u8> // all fields at defaults: a valid, empty buffer

A field’s default is its = expr initialiser if it has one, otherwise the zero value of its type. Defaults are evaluated in declaration order. A reference has no zero value, so a view struct holding one has no defaults: it is built by a literal or a constructor, which binds the reference first (§6.7).

let f: File is legal only if File can be default-initialised — that is, if it has no constructor, or has one callable with zero arguments. Otherwise the error names the constructor you have to call. [PROPOSED]

This is what makes the comment in your Buffer::copy true and checkable: copy always runs on a defaults-initialised object, so an early return out of it leaves a valid empty value rather than a half-built one.

A type is copyable if:

  • it has an impl Copy that applies to it — impl<T: Copy> Copy for Buffer<T> covers Buffer<f32> and not Buffer<File> — or
  • it implements neither Copy nor Drop, has no own field, and every field is copyable.

Every copyable type satisfies a T: Copy bound, whether or not it has an impl Copy. Otherwise it is move-only, and assignment or passing by value requires an explicit move:

struct File {
handle: own ^u64
}
impl Drop for File {
fn drop(&mut self) { close(handle) }
}
let f = File("save.dat")
let f2 = f // error: File is move-only because it owns a resource.
// help: use `move(f)` to transfer ownership, or implement
// `Copy` for File to make it copyable.
let f3 = move(f) // ok

own on a field is what flips the switch. It is not a comment — it is the declaration that this struct is responsible for a resource, and it makes the whole struct move-only.

own describes the struct, not the pointer. An own ^T value is one pointer, and reading one — comparing it to null, dereferencing it, handing it to a release function — copies a pointer and nothing else. What is move-only is the struct that declares the field, because that is the thing with a drop to run. Anything else would make if handle != null inside File::drop a move out of self. [DECIDED]

Acquiring ownership is an ordinary assignment: a plain ^T may be stored into an own ^T field, which is what keeping the pointer a C library’s create function or malloc returned (§14, §15) has to mean. The reverse is also allowed — an own ^T satisfies a ^T — so own narrows nothing at the use site. [DECIDED]

move(x) transfers ownership out of x. Afterwards x is moved-from, and the compiler tracks that with flow analysis:

let a = String("hello")
let b = move(a)
println!("{}", a.length()) // error: `a` was moved on line 2 and cannot be used

The analysis is flow-sensitive and handles branches:

let s = String("x")
if cond {
consume(move(s))
}
use(s) // error: `s` is moved on some paths reaching here

At runtime a moved-from value is left at its defaults — not in some unspecified state. This is a deliberate divergence from C++: the source of a move is reset to exactly the state a freshly declared value would have, which means running its drop is always safe and always a no-op for the resource. It costs a few stores; it removes an entire category of bug. [DECIDED]

This holds for every type. move on a copyable value — an i32, a &str — is still a move: the source is moved-from afterwards, a later use of it is an error, and at runtime it is reset to its defaults. To go on using a copyable value, copy it instead: leave out move. One rule for every type is also what keeps a generic body honest: move(v) in a Buffer<T> means the same for Buffer<i32> as for Buffer<File>. [DECIDED — §18.33]

A move needs a place the function owns. move resets its source, and a &, a ^ or an array element is not something the function has the right to reset — so move(p^), move(r.field) through a reference, and move(xs[i]) are errors. A local, a by-value parameter, and a field of either are fine. [DECIDED]

The same flow analysis handles use-before-assignment for the cases where default initialisation does not apply (a let with no type and no initialiser is rejected outright; a partially-moved struct field is tracked per field), and definite return: a function that promises a value must produce one on every path. An exhaustive switch counts as returning if every arm does, since §10.3 already proved there is no other path.

What is not checked: whether a ^T made in unsafe dangles. Whether a &T is used after a call moved or freed what it points at is checked, by ties (§6.7); Dazzle had no lifetime system until D-0091, and ties are a light one: since a reference is never stored in a field (D-0003), they never appear on a struct that holds references. [DECIDED; revised by D-0091]

A type’s drop runs deterministically:

  • at the end of the enclosing scope, in reverse declaration order;
  • before a value is overwritten by assignment;
  • when the owner of the value is itself dropped (fields drop in reverse declaration order);
  • on a moved-from value — but harmlessly, since the value is back at its defaults.

defer { ... } runs a block at scope exit. A scope’s drops and its defers run together, in reverse order of declaration: a defer runs after the drops of the locals declared after it and before those of the locals declared before it, so every local it can name is still alive. It is for the non-RAII cases (defer profiler::end_zone()).

let l = Loud{ name: String("first") }
defer { println!("defer sees {}", l.name) }
let m = Loud{ name: String("second") }
// at the scope's end: drop second, defer sees first, drop first

A temporary is dropped at the end of its statement — a value a call or a literal makes that nothing binds, such as the String in String(name).as_cstr(). (&make() written out is the exception: it lives to the end of the block, §14.2.) So a view into a temporary — a &str, &cstr, slice, reference or pointer taken from it through a method, a field, an index or a slice — may be handed to a call in the same statement, which finishes first, but keeping one is an error (DZ0407): by let, by assignment, or by return. Bind the owner first, let owned = String(name), and take the view from it. A function’s result is not followed into its arguments, since what it returns need not point into them. [DECIDED — D-0081]

A type with an own field and no drop is an error. [DECIDED — §18.4]

error[DZ0412]: `File` owns a resource but never releases it
--> src/io/file.dz:2:5
|
2 | handle: own ^u64
| ^^^^^^^^^^^^^^^^ this field is marked `own`
|
= help: add an `impl Drop for File` that releases `handle`, or remove `own` if File
does not own this pointer

Everything above becomes ordinary C++ special members, which is why Dazzle does not have to implement destruction ordering, temporary lifetime, or exception safety itself — C++ already does, correctly, and every C++ debugger and profiler already understands it.

Dazzle Generated C++
Buffer(cap) Buffer(uint32_t cap)
impl Copy’s copy Buffer(const Buffer& other) — body runs on a defaults-initialised *this
impl Drop’s drop ~Buffer()
any other impl’s method a free function taking the receiver first: Mul_f32__Vec2__mul(const Vec2& self, float rhs)
move Buffer(Buffer&& o) — memberwise steal, then reset o to defaults
copy assignment ~Buffer(); new (this) Buffer{}; copy(other); + self-assignment guard
move assignment ~Buffer(); new (this) Buffer{}; steal(o); reset(o);
move-only type copy constructor and copy assignment = deleted

The copy-assignment lowering is exactly the drop() + reset-to-defaults + copy() sequence your Buffer comment describes, so the invariant it relies on is guaranteed by codegen.

Deleting the C++ copy constructor for move-only types means that even if Dazzle’s own flow analysis had a hole, the C++ compiler catches the copy as a second line of defence.

A reference must never be used after the memory it points into has moved or been freed. Ownership (§6.4) already rules out a use after a move; ties rule out the other half: a reference into storage that a call has since grown, shrunk or replaced. [DECIDED — D-0091]

let n = counts.get_or_insert(word, 0)
counts.insert(other, 1) // may grow the table
n += 1 // error: `insert` kills `counts@items`, and `n` points into it

A tie is a span during which some memory is valid, written @name. A struct declares its ties among its members, tie @name = path, …, saying what memory of its own each one stands for. The paths start at the struct’s fields, private ones included: a tie is a public name for that private memory, so a signature can mention it without exposing a field (§4). A path ending at a pointer field names the memory the pointer owns; one ending anywhere else names everything there. A tie naming plain data, numbers or an array of them, names those bytes themselves, inside the value: a short String’s text.

pub struct List<T> {
m_data: own ^mut T = null
...
tie @items = m_data // the array of elements
tie [*] = m_data[*] // the elements in it
}
pub struct String {
m_store: [2]usize = [0, 0] // short text in place, or where long text is
...
tie @text = m_store
}
pub struct HashSet<T> {
m_map: HashMap<T, bool>
tie @items = m_map@items // another type's tie
tie [*] = m_map[*]
}

A tie belongs to the type, not to whoever uses it: it is not a generic parameter, and every List has its own. A struct declares each tie once, DZ0567 otherwise, and a base’s ties are its derived types’ too.

A tie may stand for several places, listed together. A HashMap keeps its hashes, keys and values in three arrays that grow as one, so tie @items = m_hashes, m_keys, m_values; a SlotMap, three lists that change together, has tie @items = m_values@items, m_slot_of@items, m_slots@items. A path naming the tie names all of them, and they live and die together: a kill through any one ends a view into another, and a clause naming the tie is exact for a body that kills only one of them. A path goes on past such a tie only where every place under it has the same type.

tie [*] says where the elements are: the memory [*] names (below) for a type with an Index or an Iterable implementation. Inside List, the elements are m_data[i], behind a pointer; tie [*] = m_data[*] is what makes &m_data[i] a view in self@items whose element is self[*], so growing the array ends the view and leaves what the element owns alone. [*] on a pointer field names the array it points at. A type that declares no tie [*] still has [*] in its signatures; only its own body cannot follow its elements.

A tie path names memory: a parameter, self or a static, then fields visible wherever the function is, declared ties, and [*] for any element. A path that ends without a tie means every tie under it.

Path What it names
list@items the array: what &list[i] points into
list[*]@text the text of any element of a List<String>: what list[i].as_str() points into
list[*] everything every element owns
list everything the list owns

list@items and list[*]@text are separate places, but a view inside an element dies with the array: what a path reaches from an element through no pointer’s storage is in the element’s own bytes, which move when the array does. A String keeps text of up to 15 bytes in place (§14.2), so list[i].as_str() dies when the list grows, sorts or swaps, as &list[i].name does, and so does a method’s reference into an element, list[i].left_of(). What an element owns behind a pointer stays where it is: a view into one inner list of a List<List<i32>> survives pushing another.

[*] names the elements of what a for can walk: an array, a slice, or a type with an Index<usize> or an Iterable implementation. Through Index the elements have a type, so a path can go on into them (list[*]@text); a type that can only be iterated — a HashMap, whose elements are its entries — is named whole at [*], and the path stops there. So does a path at a tie standing for a pointer field, which names that memory as a whole.

Every reference carries a tie, known where it is made, except a const &str, which points into static storage that nothing kills (§5.3): &mut world.enemies[i] is in world.enemies@items. A reference parameter may say where it points, which is then checked inside the function: e: &mut Enemy in world.

So does every reference a function returns. The signature says where it points with in: pub fn first(&self) -> ^T in self@items. A result that may be in any of several places names them in parentheses, and a kill of any one ends it:

fn longer(a: &str, b: &str) -> &str in (a, b) { … }

With no in, a result is in its one view input — a reference, a pointer, a slice or a &str parameter, or self — as a whole. With more than one, the signature must say which (DZ0570); with none, a reference, slice or &str result points into nothing the caller can see die (DZ0571), and text that never dies is a const &str instead.

fn strip_prefix(&self, prefix: &str) -> ^str in self // `prefix` is an input too
fn title() -> const &str // `-> str` is DZ0571

The same holds for a result that holds a view: a struct (an iterator such as iter()’s SliceIter), a tuple or an optional with one inside, other than behind a pointer field. So let it = xs.iter(), then xs.push(1), then using it is a use after a kill, and so is a half of s.split_once("=") after s changes. Three results stay untied: one that holds a view only through a type parameter (pop(&mut self) -> T? hands back the element, which points wherever it did, not into the list), a raw pointer or ^cstr with no input, which may own what it points at, and an extern fn’s with no input, which is C’s memory.

A kills clause says which ties a function may end, after the return type:

pub fn push(&mut self, v: T) kills self@items
pub fn clear(&mut self) kills self@items, self[*]
pub fn len(&self) -> usize // no clause: kills nothing
pub fn spawn(world: &mut World, e: Enemy) kills world.spawned@items
fn rebuild(world: &mut World) kills world // anything reachable from `world`
  • No clause means the function kills nothing, and a clause says exactly what the body kills. The compiler works out what each body kills — through what it assigns, moves and drops, and the clauses of what it calls — and holds the clause to it, both ways:

    fn fill(xs: &mut List<i32>) {
    xs.push(1) // error: `push` kills `xs@items`, and `fill` has no `kills xs@items`
    }
    fn look(xs: &mut List<i32>) kills xs@items {
    println!("{}", xs.len()) // error: `look` kills nothing, but declares `kills xs@items`
    }
    fn reset(w: &mut World) kills w {
    w.enemies.clear() // error: `w` is wider than what dies, `w.enemies@items, w.enemies[*]`
    }

    A kill the clause leaves out is an error where it happens; a path the body never kills, or one wider than what it kills, is an error on the clause. Every one comes with the clause the body needs, which dz fix writes and the editor offers as a fix, and the editor shows the clause a body needs while it is being written. Every function says it, private ones included, so a signature always tells what a call can invalidate, and a change to a body never breaks a caller somewhere else without the signature changing too.

  • Where there is no body to hold it to, the clause is a promise instead: a trait method without a default, an extern fn, a function type. An implementation’s own clause is exact for its body, as anywhere, and may name no more than the trait’s; so may its result’s in, which with none written is in its one view input (DZ0573 otherwise). A call through the trait reads only the trait’s, so kills self@items under a trait’s kills self, or in self@items under a trait’s elided self, is fine, and the reverse is not.

  • A clause names only what is visible wherever the function is. A private method may name a private field (kills self.m_things@items); a pub one names a tie the struct declares for it (tie @things = m_things@items, then kills self@things).

  • A path may start at a static (§7.7), named as an expression names it: SCORES, or world::SCORES from another module. A static is one place for the whole program, so its kills are no caller’s own: every caller names them in its clause too, up to an entry point (main, a #[test] or a #[bench]), which needs no clause for them because nothing called it. A function’s clause names only a static every caller can see, so a pub function’s names a pub static (DZ0563). A result tied to one says so with in, fn first() -> &i32 in SCORES.list@items. A kill of a static inside unsafe is vouched for there and is no kill a clause must name. [DECIDED — D-0094]

  • A trait method’s clause is the most an implementation may kill, and a function type’s is the most a function stored in it may; a function type names its parameters when its clause does: fn(world: &mut World) kills world. There, more than one path is parenthesised, kills (w.a@items, w.b@items), since a comma after a bare path would begin the next parameter of whatever the type is written in.

  • A function fits a function type whose clause covers its own: every path the function’s clause names is one the type names, or under one, parameter by position. A function type with no clause promises to kill nothing, so only a function that kills nothing fits it (DZ0568 otherwise), and a call through a value of the type kills what the type’s clause names, at the call’s arguments:

    fn spend(l: &mut Ledger) kills l.lines { l.lines.push(1) }
    fn run(f: fn(l: &mut Ledger), l: &mut Ledger) { f(l) }
    run(spend, l) // error: `spend` kills `l.lines`; the type, nothing
    fn run(f: fn(l: &mut Ledger) kills l.lines, l: &mut Ledger) kills l.lines { f(l) }

    The clause is not part of the type’s identity: two function types that differ only in it are one C++ type. A pointer to a C function has no clause, and fits only a type with no &mut or ^mut parameter, which can kill nothing.

  • A function type says where its result points, as a declaration does: with in after the result, fn(a: &Enemy, b: &Enemy) -> &Enemy in (a, b), or in its one view input when it writes none. Several view inputs and no in is DZ0570, and a view from none is DZ0571, as for a declaration. A call through a value of the type gives a result tied as the type says, and a function fits only a type whose in covers its own, parameter by position (DZ0572 otherwise): first(a, b) -> &T in a fits -> &T in (a, b), and not the other way round. An in after a function type’s result belongs to the type, never to a parameter holding it.

  • A call with no clause to read — a C or C++ function, a pointer from C — kills everything reachable through its &mut and ^mut arguments.

What kills what.

  • Moving or dropping a place kills every tie under it, inline storage included.

  • Writing a place kills what the old value owned, and its payload. Assigning over a value ends every view into memory it owned — a String’s text, a List’s array — and, for a tagged union or an optional, every reference into its payload, since the variant may change. A plain value is overwritten where it is, so a reference into it stays valid, as it does after p.x += 1:

    let x = &body.at.x
    body.at = Vec2{ x: 1, y: 2 } // fine: `x` still points at `body.at.x`; kills nothing
    let s = body.name.as_str()
    body.name = String("new") // kills `body.name`: the old text is freed

    A clause reaches as far as a write does: a function given &mut w can free what w owns or change its payloads, but cannot move w, so kills w leaves a reference to a plain field of w alone.

  • Elements share their container’s ties, unless their indices are known apart. A kill through xs[i] is a kill through xs[j], since i may equal j. Two indices are known apart when both are literals that differ (xs[0], xs[1]), or both are the same local that is not mut plus or minus different literals (xs[i], xs[i + 1], xs[i - 1]). Such a local keeps its value until its binding runs again, as a for loop’s does on the next pass, and from then on an element it indexed is any element. So appending one string of a list to its neighbour compiles, where the same element, or two indices not known apart, is refused:

    for i in 0..names.len() - 1 {
    let s = &mut names[i]
    s.push_str(names[i + 1].as_str()) // fine: `names[i + 1]` is another element
    }
    names[0].push_str(names[0].as_str()) // error: the text read may move as it grows

    A clause still names elements as [*]: a function cannot say which element it kills. [DECIDED — T-0168]

  • A call kills what its clause names, at the call’s arguments. At a call, a reference in a tie that the callee’s clause kills is dead afterwards, and using it is an error:

    fn think(e: &mut Enemy, world: &mut World) kills world.enemies@items {
    world.enemies.push(Enemy::spawn(e.x))
    e.hp -= 1
    }
    think(&mut world.enemies[i], &mut world) // error: `e` points into `world.enemies@items`
    think(&mut boss, &mut world) // fine: `boss` is not in `world`
  • A call may reach what it kills through one argument only. A function is checked once, for every caller, with its parameters taken as separate places, so at a call, memory the clause kills may be reached through the argument the clause names it by, and no other (DZ0420): not by one pointing at what owns that memory, nor by a second argument naming the same kill. Arguments may still share memory the call does not kill, and memory that cannot die, such as an i32 field:

    fn append_copy(dst: &mut List<String>, src: &List<String>) kills dst@items { … }
    append_copy(&mut ys, &ys) // error: `ys@items` dies through `dst`, and `src` reaches it
    append_copy(&mut ys, &zs) // fine
    fn rebuild(world: &mut World, score: &i32) kills world.names@items { … }
    rebuild(&mut world, &world.score) // fine: an `i32` owns nothing that can die
    total(&xs, &xs) // fine: `total` kills nothing

    [DECIDED — D-0094]

  • A value with a drop uses its views when it is dropped, at the end of its scope or when it is assigned over, since its drop may read them where no line shows it. A view struct with a drop, dropped after what it views was killed, is DZ0413; one with no drop, such as an iterator, is not.

A view lives only where a reference does (D-0093): in a local, a parameter, a result or a temporary, never in a field, a container or a type argument. A struct that must hold one, such as an iterator, is declared view struct, and is then a view itself, placed by the same rule; std’s iterators are view structs. A generic that may be given a view says so with view on the type parameter, and its body treats a value of it as one; a trait’s associated type that an implementation may choose a view for is a view type, as Iterate::Item and Iterable::Iter are. A plain type parameter is never a view.

view struct Numbered<view I: Iterate> {
pub inner: I
m_index: usize = 0
}
impl<view I: Iterate> Iterate for Numbered<I> { … }
fn count<view T: Iterate>(it: T) -> usize { … }
count(xs.iter()) // fine: `T` may be a view
fn keep<T>(x: T) -> T { … }
keep(xs.iter()) // error: `SliceIter<i32>` is a view, and `T` is not `view`
let its = List<SliceIter<i32>>() // error: a container's element is no place for a view

A struct with a view parameter must be a view struct, and an implementation whose type’s view parameter it reaches takes that parameter as view too. Neither a union’s payload nor a static holds a view. All of these are DZ0517, the same as a &T field of a struct that is not a view struct.

A view struct’s tie on a view field names what the field views, not the field: a method returning through it gives a result that outlives the view struct, in whatever the struct was made from.

pub view struct SliceIter<T> {
m_slice: &[]T
m_index: usize = 0
tie @items = m_slice
}
impl<T> Iterate for SliceIter<T> {
fn get(&self) -> &T in self@items { return &m_slice[m_index] }
…
}
fn first(xs: &List<i32>) -> &i32 {
let it = xs.iter()
return it.get() // fine: in `xs`, not in the local `it`
}

A struct’s views are one tie (D-0093, point 3), so such a result is in everything the struct views. std’s SliceIter, SliceIterMut, Split, Lines and SplitWhitespace declare theirs, and so do the map iterators, which hold a reference.

A view struct may hold a reference, &T or &mut T, and is placed as one is, so the reference still lives only where references live. A reference has no zero value, so such a struct has no defaults (§6.2): a literal names each reference field, a binding of the type needs a value (let it: HashMapIter<K, V> alone is an error), and a reference field takes no = default. A constructor binds each reference field in its opening statements, before any other: there, m_map = map makes the field refer to map; anywhere else, assigning to it writes through it, as for a local reference (§5.2), and through a &T that is an error. A reference field may not refer into the struct that holds it, which a copy or a move would leave behind. Each of these is DZ0574. A tie on the field names what it refers to:

pub view struct HashMapKeys<K: Hash + Eq, V> {
m_map: &HashMap<K, V>
m_slot: usize = 0
tie @items = m_map
HashMapKeys(map: &HashMap<K, V>) {
m_map = map // binds: the first statements
m_slot = first_entry_from(map, 0)
}
}
impl<K: Hash + Eq, V> Iterate for HashMapKeys<K, V> {
fn get(&self) -> &K in self@items { return m_map.key_at(m_slot) or unreachable }
…
}

A constructor’s result, like a function’s, is in its one view input, so HashMapKeys(&map) is in map.

A view struct given to a function stands for what it views, as a slice or a &str does. A view input is a reference, a pointer, a slice or a &str parameter, or self, and also a view taken by value: a view struct, or a value of a view parameter. A result elided to a view struct, or in it whole, is in what the struct views, so it outlives the struct, and a temporary iterator’s item can be kept:

fn first<view T: Iterate>(it: &T) -> T::Item { return it.get() }
let a = first(&xs.iter()) // in `xs`; dies when `xs` grows
fn at_of(p: &Peek) -> &usize { return &p.at } // error: `at` is `p`'s own
fn at_named(p: &Peek) -> &usize in p.at { return &p.at } // fine: tied to `p` itself

What such a result may not be is the struct’s own storage — a field holding no view, or the struct itself behind a reference — since a caller ties it to what the struct views; naming the field in in ties it to the struct instead (DZ0414 otherwise). A result of a view parameter’s type, or of a projection of a view type (T::Item), holds a view by the same rule, and is tied as one. view is a keyword only before struct, type and a type parameter, so let view = camera.view_matrix() still means what it did. Text is a view by this rule, &str and ^str (§5.3); slices become views with T-0110, and until then are values.

What stays allowed is aliasing. &mut xs[i] beside &xs[j], a write through a &mut that moves nothing (p.x += 1), a borrow of one field while another changes: none of these kills anything. A use is an error only when a call in between can move the memory the reference points into. Dazzle is single-threaded until §18.11 is decided, which is what lets it keep aliasing that Rust refuses.

What stays unchecked is what unsafe vouches for (§1.1): a pointer made inside unsafe by a cast or arithmetic, and a pointer stored in a struct (a context struct’s ^mut World, §5.2), since the checker cannot know that it and a separate &mut world are one World. Using either needs unsafe. A pointer a safe call returns is tied like a reference. std’s own clauses are written by hand over unsafe code, and are trusted, and checked at run time in debug builds (§14.2).

The ties of std’s containers:

self@items self[*]
push, insert, reserve kills —
pop, remove, swap_remove kills kills
clear, truncate; assigning or dropping the container kills kills
sort_by, swap, reverse kills —
len, get, first, indexing — —

Taking an element out kills self[*]. The element leaves with its own memory, which stays valid while the value handed back lives; but a view into it was made through the container, and nothing can follow it to that value and end it when the value is dropped:

let s = names[0].as_str()
let x = names.swap_remove(0) // kills `names[*]`: `s` may point into `x`'s text
use(s) // error, though `x` still lives; it may not by then

Since every element shares [*], a view into another element’s text ends too. Moving elements within the container (swap, sort_by) hands none out, so it leaves [*] alone.

String’s push_str and push kill self@text. HashMap, HashSet and SlotMap follow List; a map’s insert and replace kill self[*] as well, since a value they replace is handed back or dropped.

The checker reports a use after a kill (DZ0413), a return outside its in (DZ0414), and a clause that leaves a kill out, names one that never happens, or names more than dies (DZ0415–DZ0417), each clause error with the clause the body needs, which dz fix writes; a returned view that does not say where it points (DZ0570, DZ0571); and a function stored where its function type’s clauses promise less than its own (DZ0568 for kills, DZ0572 for in). A debug build also checks std’s trusted clauses at run time (T-0116, r102).