ADR-118: with Is a Copy of a Value With Named Fields Changed

Status: Accepted Date: September 16, 2026 Related: ADR-298 D8 (the word was kept for this), ADR-298 (a word is reserved for a construct), ADR-094 and ADR-282 (a value is moved or viewed as the compiler decides, and a copy is written), ADR-281 (update fn(mut v), the same idea for a value behind a lock), Part I 2.1 (mut), 4.1 and 4.2 (structs and their literals), 9.2 (private fields, NK1110)

1. Context

Every binding in this language is immutable unless it says mut, and the value a program wants most often is the one it has, with one field different. There was no way to write that short of a struct literal naming every field again:

let moved = Point(x: p.x + 1, y: p.y, z: p.z)

Three fields are bearable; a Stats with six is a line nobody reads for its one change. C# has with for records, Kotlin has copy(x = …), Rust has Point { x: 1, ..p } — and this language had the word reserved, unused, for exactly this shape.

2. Decisions

D1 — value with { field: expr, … } is a new value of the same type, with the named fields changed and the rest taken from value.

let moved = p with { x: p.x + 1 }
let stats = old with { count: old.count + 1, sum: old.sum + t }
let named = user with { name }          // shorthand: the local `name`

The braces are the struct literal’s, with its field list and its shorthand; a field named twice, a field the type does not have, and a with that names no field at all are refused, the last because a copy that changes nothing is a line the reader would puzzle over. The left operand is any expression of a struct type — a name, a field, a call.

D2 — Only the top level. A nested change is a nested with.

p with { pos.x: 1 } is not a form. A field of a field is changed by changing the field: p with { pos: p.pos with { x: 1 } }. One rule, one level, and the question of what a path means through a T?, an enum or a list never has to be answered.

D3 — The fields not named are moved, never copied unseen.

What with does not name it takes from the left operand, by move — Rust’s ..p below. Where every such field copies (numbers, bool, char, a view), the operand is usable afterwards, as it is anywhere. Where one does not — a text, a list, a handle — and the operand is used again, the refusal names the copy to write, exactly as a value used after a move is refused elsewhere. No copy is inserted that the program did not write (ADR-282 D7).

D4 — A field may be named only where a literal could name it.

Across a package boundary with reaches pub fields and nothing else, the rule a struct literal has (NK1110). A type with a private field can still be copied with a public one changed, because the private field is not named, only carried.

3. Consequences

4. What this does not decide

5. Refusals

This site is open source. Improve this page.