6. Value semantics: ownership, copy, move, drop
This is the heart of the language. Everything else is ordinary.
6.1 The three special members
Section titled “6.1 The three special members”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).
6.2 Default initialisation
Section titled “6.2 Default initialisation”Every value is always initialised. There is no uninitialised memory in Dazzle.
let f: File // fully constructed, fields at their declared defaultslet n: i32 // 0let p: ^Player // nulllet b: Buffer<u8> // all fields at defaults: a valid, empty bufferA 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.
6.3 Copyability
Section titled “6.3 Copyability”A type is copyable if:
- it has an
impl Copythat applies to it —impl<T: Copy> Copy for Buffer<T>coversBuffer<f32>and notBuffer<File>— or - it implements neither
CopynorDrop, has noownfield, 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) // okown 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]
6.4 Move semantics
Section titled “6.4 Move semantics”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 usedThe 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 hereAt 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]
6.5 Drop
Section titled “6.5 Drop”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 firstA 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 pointer6.6 How this lowers to C++
Section titled “6.6 How this lowers to C++”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.
6.7 Ties and kills clauses
Section titled “6.7 Ties and kills clauses”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 tablen += 1 // error: `insert` kills `counts@items`, and `n` points into itA 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 toofn title() -> const &str // `-> str` is DZ0571The 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@itemspub fn clear(&mut self) kills self@items, self[*]pub fn len(&self) -> usize // no clause: kills nothingpub fn spawn(world: &mut World, e: Enemy) kills world.spawned@itemsfn 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 fixwrites 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’sin, which with none written is in its one view input (DZ0573otherwise). A call through the trait reads only the trait’s, sokills self@itemsunder a trait’skills self, orin self@itemsunder a trait’s elidedself, 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); apubone names a tie the struct declares for it (tie @things = m_things@items, thenkills self@things). -
A path may start at a static (§7.7), named as an expression names it:
SCORES, orworld::SCORESfrom 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 apubfunction’s names apubstatic (DZ0563). A result tied to one says so within,fn first() -> &i32 in SCORES.list@items. A kill of a static insideunsafeis 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 (
DZ0568otherwise), 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, nothingfn 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
&mutor^mutparameter, which can kill nothing. -
A function type says where its result points, as a declaration does: with
inafter 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 noinisDZ0570, and a view from none isDZ0571, 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 whoseincovers its own, parameter by position (DZ0572otherwise):first(a, b) -> &T in afits-> &T in (a, b), and not the other way round. Aninafter 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
&mutand^mutarguments.
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, aList’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 afterp.x += 1:let x = &body.at.xbody.at = Vec2{ x: 1, y: 2 } // fine: `x` still points at `body.at.x`; kills nothinglet s = body.name.as_str()body.name = String("new") // kills `body.name`: the old text is freedA clause reaches as far as a write does: a function given
&mut wcan free whatwowns or change its payloads, but cannot movew, sokills wleaves a reference to a plain field ofwalone. -
Elements share their container’s ties, unless their indices are known apart. A kill through
xs[i]is a kill throughxs[j], sinceimay equalj. Two indices are known apart when both are literals that differ (xs[0],xs[1]), or both are the same local that is notmutplus or minus different literals (xs[i],xs[i + 1],xs[i - 1]). Such a local keeps its value until its binding runs again, as aforloop’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 growsA 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 ani32field: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 itappend_copy(&mut ys, &zs) // finefn rebuild(world: &mut World, score: &i32) kills world.names@items { … }rebuild(&mut world, &world.score) // fine: an `i32` owns nothing that can dietotal(&xs, &xs) // fine: `total` kills nothing -
A value with a
dropuses its views when it is dropped, at the end of its scope or when it is assigned over, since itsdropmay read them where no line shows it. A view struct with adrop, dropped after what it views was killed, isDZ0413; one with nodrop, 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 viewfn 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 viewA 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` growsfn at_of(p: &Peek) -> &usize { return &p.at } // error: `at` is `p`'s ownfn at_named(p: &Peek) -> &usize in p.at { return &p.at } // fine: tied to `p` itselfWhat 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 textuse(s) // error, though `x` still lives; it may not by thenSince 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).
