ADR-083: A Field of a Borrowed Subject May Not Be Handed Out by Value

Status: Accepted Date: September 15, 2026 Related: ADR-281 D2 (a hull you can see is one you write — the sentence that rules out the other option), ADR-005 D7 (an error explains itself in plain language), ADR-081 (the concatenation on the same line), ADR-views-3 D7 (where the view is the compiler’s)

1. Context

impl User {
    fn name_of(ref self) -> String {
        return self.username
    }
}
error[E0507]: cannot move out of `self.username` which is behind a shared
              reference

about a file nobody wrote. That is every accessor over a field that is not a number: ref self is what Part I 4.2 writes for a method that only reads, and a String, a Vec or a struct this program declares is moved rather than copied in the language below.

A class, because the instance is never the point. A field of a borrowed subject leaks in exactly three positions, and three others work:

   
return self.name E0507
let x = self.name E0507
takes(self.name) E0507
return self.count, an i64 compiles
return self.name.clone() compiles
return self.name from fn m(self) compiles

2. Decisions

D1 — It is refused here, in this language’s words. NK1131.

Part I 6.8 decides this and it decides it already:

An ownership rule refuses a program that breaks it. Every such refusal explains itself in plain language and names the way out. Reading a Nikaia diagnostic requires no knowledge of Rust; a backend error that reaches user code is a defect of the compiler.

So the page does not ask whether this program should be rejected — it says an ownership rule does that — and it says where the rejection has to come from.

D2 — Not a .clone() the emitter writes.

Changed by ADR-views-3 D7: where the method takes ref self and declares a view as its result, return self.field is lent by the compiler and not refused, so the message’s ways out apply only where a field is moved, bound or passed.

That was the other option and it is against a decision already made. ADR-281 D2 wrote a hull you can see is one you write, and a copy the author cannot see is the same thing one position over: it would put an allocation in the program’s hot path that nothing in the source accounts for, in a chapter whose whole argument is that a value’s cost is visible.

And it would buy nothing, because the working program already exists and is one word away. Both ways out are in the message: .clone(), written where it happens, or a self receiver where the method is meant to consume its subject.

D3 — Asked only where the field’s type is known and does not copy.

A number, a bool, a char and any view are copied rather than moved, so handing one out takes nothing away. A field this compiler cannot type says nothing at all, which is Part III C.4’s polarity: a refusal that fires where it should not is worse than the leak it replaces.

The rule therefore reads the field’s type rather than counting ref selfs, and the positions are the three in §1 rather than every place an expression can stand. A position missed lets rustc answer, as it would without this rule; a position wrongly included breaks a program that works.

3. Consequences

Every accessor gets an answer in this language. The message names the field, what was done with it, the type that made it a move, and both ways out.

Part I 4.7 cannot write return "User: " + self.username, and that is visible rather than hidden: it type-checks (ADR-081) and is refused by this, correctly. The page keeps the interpolated form, which borrows.

Reading a field is not taking it, and that is the property the polarity rests on: self.name.to_uppercase(), self.name.len() and f"{self.name}" all borrow and all still work.

4. What this does not decide

D4 — The message leads with the view, because it is the one that costs nothing.

A written .clone() and a self receiver each take something: an allocation, or the whole subject. A view costs nothing and is what a reader of Part I 6.5 would reach for, so it goes first and it names the type the field actually has: ref String for text, ref Vec[i64] for a list of numbers.

A message that names a way out has to be a way out. Without a ledger entry for .clone(), sync::infer cannot vouch for the call: the method around it comes out as one that pauses, and ADR-080’s NK1129 refuses it for disagreeing with its trait — take NK1131’s advice and a different diagnostic fires. So String::clone is in std.contracts.

This site is open source. Improve this page.