mut parameter around a call, with no cloneOpen and answer this issue on GitHub.
ready · opened 2026-10-29 · updated 2026-11-03 · 4 comments
Found while building #950 (b950e report). Site: tools/emit_stmt.nika 896, let outer = walk.at_field?.clone().
Probe (inside a for, where walk is a mut parameter):
let outer = walk.at_field
walk.at_field = P { .. }
visit(walk)
walk.at_field = outer
It is refused twice:
let inside the loop binds a part declared outside it.outer is a view of walk (#962).In Rust this is let outer = mem::replace(&mut walk.at_field, P {..}); visit(walk); walk.at_field = outer;, with no copy.
Kind 2: ADR-views-1 D5 names mem::take and mem::replace with a stand-in for a part handed to a keeper (#949). It does not say whether moving a field of a mut parameter out by let and giving it back is a take/give-back too. The question for the owner: should a let that moves a field out of a mut place, followed by giving it back by assignment before the block ends, be read as take/give back (lowering to mem::replace), rather than refused as a view? Until it is decided, the .clone() stays.
let that takes a field out of a mut place, to be given back (architect, 2026-10-29)A function that walks a tree often carries a state with it (walk), and for one call it wants a field of that state to say something else, then the old value back:
for field in list {
let outer = walk.at_field // keep the old value
walk.at_field = EmitPair { .. } // put a new one in
self.emit_core_block(world, walk, ..) // the call sees the new one
walk.at_field = outer // put the old one back
}
Rust writes this as let outer = mem::replace(&mut walk.at_field, new); …; walk.at_field = outer;. It copies nothing. Python and Swift write the four lines above.
Today (checked with target/fast/nikaia check on develop 50e18063, probe with walk: W a mut parameter and at: P?):
for: NK2105 walk.at is bound to outer by a let inside a loop, but it was declared outside it; and twice NK2305 walk is changed here, but outer may point into its data;NK2305.So the program writes let outer = walk.at_field?.clone() (tools/emit_stmt.nika 896). That copies two strings per field, per unrolled loop.
Where it comes from: the #950 builder (b950e report). It is not a single site. In crates/nikaia-std/src/tools/*.nika there are 18 places that write let x = a.f.clone() and later a.f = x (counted by script: same name, same field, within 80 lines). More such let … .clone() lines may turn out to be this pattern when looked at (61 in all).
mut parameter is given back before the function is left. Between the taking and the giving back stands no throw and no call that can fail (NK2108). Assigning the part gives it back.push, an insert, a let that moves it“. D3 lowers a taking as mem::take, or as mem::replace with a stand-in. D5: no mem module, no take(). D2: a view’s block is whole again at its end and at every continue and break.let over a place … is a view of it where the value would otherwise have to move.”So D1/D5 cover the intent: the form is meant to exist, and there is no second spelling. What they leave open is which of the two sentences wins for let outer = walk.at_field. The checker reads it as a view, so the assignment after it is NK2305. D1 never says when a let over a place “moves it” rather than views it. The loop case is not settled either: ADR-094 D7 speaks of the function’s ways out, not of a loop turn. So the next turn’s NK2105 (“it’s already gone”) still fires, even though the part is whole again before the turn ends.
1. Decided by what the name is used for, as for a parameter (inferred). A let over a part of a mut parameter or of a let mut view is a view while the name is only read. It is a taking (ADR-views-1 D1) when the name is kept: handed to a keeper, returned, or assigned into a place (walk.at_field = outer). The rule is the one 6.5 gives a parameter: a view unless the body keeps it. A taking inside a loop of a place declared outside it must be given back before the turn ends (its end, continue, break), as views-1 D2 already says for a view’s block. Then the loop’s NK2105 does not apply to it.
2. A written form: let outer = take walk.at_field, or std swap(a, b) / replace(place, new).
3. Keep the .clone().
| Simple as a scripting language | Rust lowering | Checker effort | What self-hosting gains | If it is wrong | |
|---|---|---|---|---|---|
| 1 | the four lines a Python user writes are the program; nothing new to learn | let outer = mem::take(&mut walk.at_field) (an Option, a list, text: nothing allocated) or mem::replace with D3’s stand-in. The assignment that follows can be folded into one mem::replace(&mut .., new) by the lowering, at no cost to the program |
the view/take choice for a let over a place reads the name’s later uses (the keeps walk already has them). NK2108’s walk gains the loop turn as a way out. The loop branch of hands_over_path_of skips a part that a_part_taken_out already marked |
18 sites lose a copy per turn; emit_stmt 896 and the walkers’ save/restore read as written |
a name that the program only meant to read, but then assigns somewhere, becomes a taking. A later read of walk.at_field before the give-back is then NK2105, where today it is NK2305. Both are refusals, and the message must name the assignment that made it a taking |
| 2 | a word to learn for what the four lines already say; take contradicts views-1 D5 |
the same | a parser form and a std entry |
the same 18 sites, rewritten | reopens D5 (two forms for one thing) |
| 3 | the program reads wrong (a copy for nothing) | .clone() |
none | none; 18 copies stay | - |
Risk, named honestly: under option 1, what a let is depends on a line further down. That is already true of parameters (6.5) and of the ledger, and the message is what makes it bearable. It is the only real cost.
Option 1. It is what ADR-views-1 D1 already says (“a let that moves it”), made precise. It adds no syntax and keeps D5. It needs one sentence in Part I 6.5 and one in ADR-views-1, so the version moves with it.
ADR-views-1 D6 (new). A
letover a part is a view while it is read, and a taking when it is kept. Aletthat binds a part of amutparameter, ofref mut selfor of alet mutview is a view of it, unless the name is kept: handed to something that keeps it, returned, or assigned into a place. Then thelettakes the part (D1), and D2’s rule holds. Inside a loop, a part taken out of something declared outside it is given back before the turn ends (its end,continue,break). It is then not handed over again on the next turn, andNK2105’s loop case does not apply.Part I 6.5, after “…is a view of it where the value would otherwise have to move”: “…, and takes the part where the name is kept and the place is a
mutparameter or alet mutview:let outer = walk.at; walk.at = P {..}; visit(walk); walk.at = outercopies nothing.”
If the owner decides option 1, the build is: the checker’s view/take choice, NK2108 for the loop turn, the lowering’s mem::take/mem::replace, a test (the probe above, with .stdout), and then the 18 sites moved off .clone().
Generated by Claude Code
Owner’s decision (2026-10-29): option 1 is accepted.
let over a part is a view while the name is only read.mem::take or mem::replace. There is no new syntax.Generated by Claude Code
A let over a part of a mut parameter, of ref mut self or of a let mut view is a view while the name is only read. It takes the part (D1) when the name is kept: handed to a keeper, returned, or assigned into a place. Inside a loop it is given back before the turn ends, and NK2105’s loop case does not apply. Every refusal that comes from the taking names the let, the place and the line that keeps the name. Lowering: mem::take / mem::replace with D3’s stand-in, and a directly following assignment of a new value may be folded into one mem::replace. No syntax. (The version raise is the supervisor’s.)
let over such a part, read the name’s later uses (the keeps walk has them). Kept means a taking: no NK2305 for the later change of the place. Files: views.nika, keeps.nika, view_changes.nika, check_takes.nika (a_part_taken_out, and the loop branch of hands_over_path_of skips a part already marked taken).continue and break for a part taken inside a loop out of something declared outside it. NK2105/NK2108 messages name the keeping line, e.g. “walk.at_field was taken out by let outer at 12, because outer is kept at 15”. Files: check_takes.nika, check_may_fail.nika.let as std::mem::take(&mut <place>) or std::mem::replace(&mut <place>, <stand-in>), optionally folded with the next assignment. Files: emit_core_places.nika, emit_stmt.nika.tests/compiler/tests/: the #979 probe with .stdout, in a for and outside one; a refusal (a read of the part between the taking and the give-back) with .stderr naming the keeping line; and a turn that continues with the part out (NK2108)..clone() sites in crates/nikaia-std/src/tools/. A script counts 35 candidates today (let x = a.f….clone() with a.f… = x within 80 lines). Only a site where the place gets a new value before anything reads it (put new, call, give back) is this form. A site where the walk in between reads the old value (the branch joins of bounds_walk: proven/written/poisoned) needs its copy and keeps it. Check each site; do not force any.
emit_stmt.nika 172, 896; emit_core_types.nika 124 (lane E)check_function.nika 731-745 (7); check_core_call_on.nika 1074, 1077; check_core_comptime.nika 266; check_pauses.nika 80 (lane C)text_tiers.nika 3483, 4056, 4263, 4270, 4750 (lane T)bounds_walk.nika 2119-2243 (11, mostly copies that must stay); ledger_ops.nika 120; prover_scope.nika 217; throws.nika 593; views.nika 2235, 2246 (free)
Each moved .nika regenerates its lowered/*.rs.When it can start: steps 1-2 touch view_changes.nika and keeps.nika (lane T) and check_takes.nika/check_may_fail.nika (lane C), and step 3 touches emit_* (lane E). So it starts once lanes T, C and E have committed, all of them. Step 5 then goes file by file.
Generated by Claude Code
Two more probes for the build, on the receiver D6 names besides a mut parameter: ref mut self, a field and an indexed element. Found while probing the forms a CDCL solver needs (a trail or a watch list taken out and kept); target/fast/nikaia at 31e2ea7, language 0.0.1341. (The body above still asks the question; ADR-views-1 D6 has answered it since.)
struct Solver { trail: Vec[u32], saved: Vec[Vec[u32]], count: i64 }
impl Solver {
fn stash(ref mut self) {
let t = self.trail
self.trail = []
self.count += 1
self.saved.push(t)
}
}
fn main() {
let mut s = Solver { trail: [1, 2], saved: [], count: 0 }
s.stash()
println(f"{s.trail.len()} {s.saved.len()} {s.saved[0].len()}")
}
Refused with NK1131 (this method only borrows self, although it takes ref mut self) and NK2106. Expected: 0 1 2.
struct Solver { watches: Vec[Vec[u32]], saved: Vec[Vec[u32]] }
impl Solver {
fn stash(ref mut self, lit: u32) {
let ws = self.watches[lit]
self.watches[lit] = []
self.watches[0].push(9)
self.saved.push(ws)
}
}
fn main() {
let mut s = Solver { watches: [[], [1, 2]], saved: [] }
s.stash(1)
println(f"{s.watches[1].len()} {s.watches[0].len()} {s.saved[0].len()}")
}
Refused with NK2305 (ws is a view into self). Expected: 0 1 2.
Addition to To build: both as tests with their .stdout. The neighbour without a let (the part handed straight to a keeper) is #1208.
Generated by Claude Code