Skip to content

"binding modifiers may only be written when the default binding mode is move": This sentence is baffling #143557

Open
@mcclure

Description

@mcclure

Code

Compare the following three commits.

  1. https://codeberg.org/mcc/nameless-experimental-lisp/src/commit/5bfbda4cb2e1a9760d0048bdf7d37e2fb3bcadf7 (branch 2025-06-14 commit 5bfbda4cb). This branch targets edition 2021, and compiles.

  2. https://codeberg.org/mcc/rs-bug/src/commit/1e5b33af6e364d39bffffc015f5541f6a302cde2 (branch nel-move-error-msg, commit 1e5b33af— notice this is a different git repo from other two commits). This branch targets edition 2024, and code unchanged from commit 1 does not compile.

  3. https://codeberg.org/mcc/nameless-experimental-lisp/src/commit/44e9257cff43c8db54301bff75dc8f34ede09170 (branch 2025-06-14 commit 44e9257cf). Compared to commit 2, you will see various lines changed (all lines unchanged commit 1->2) that cause the program to build again without errors/warnings.

When testing any of the above, it is adequate to just run cargo build --release.

Current output

When building "commit 2" above, the (very confusing) error message

modifiers may only be written when the default binding mode is `move`

appears ten separate times in the build output:

mcc@Anthy:~/work/r/nil-macro/dep/lisp0$ cargo build --release
   Compiling experimental-lisp-0 v0.1.4-beta (/home/mcc/work/r/nil-macro/dep/lisp0)
error: binding modifiers may only be written when the default binding mode is `move`
   --> src/memory.rs:763:31
    |
763 |                                 let MemCell::Array(_, ref mut v) = self.memory.cell_mut($to.clone()) else { unreachable!(); };
    |                                                       ^^^^^^^^^ occurs within macro expansion
...
777 |                         let addr = handle_extend!(new_array, handle, upto, idx, true, true);
    |                                    -------------------------------------------------------- in this macro invocation
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
    = note: this error originates in the macro `handle_extend` (in Nightly builds, run with -Z macro-backtrace for more info)
help: make the implied reference pattern explicit
    |
763 |                                 let &mut MemCell::Array(_, ref mut v) = self.memory.cell_mut($to.clone()) else { unreachable!(); };
    |                                     ++++

error: binding modifiers may only be written when the default binding mode is `move`
   --> src/memory.rs:763:31
    |
763 |                                 let MemCell::Array(_, ref mut v) = self.memory.cell_mut($to.clone()) else { unreachable!(); };
    |                                                       ^^^^^^^^^ occurs within macro expansion
...
786 |                                 handle_extend!(new_array, new_ast, 0, self.memory.array_len(new_ast.clone()), false, false);
    |                                 ------------------------------------------------------------------------------------------- in this macro invocation
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
    = note: this error originates in the macro `handle_extend` (in Nightly builds, run with -Z macro-backtrace for more info)
help: make the implied reference pattern explicit
    |
763 |                                 let &mut MemCell::Array(_, ref mut v) = self.memory.cell_mut($to.clone()) else { unreachable!(); };
    |                                     ++++

error: binding modifiers may only be written when the default binding mode is `move`
   --> src/memory.rs:763:31
    |
763 |                                 let MemCell::Array(_, ref mut v) = self.memory.cell_mut($to.clone()) else { unreachable!(); };
    |                                                       ^^^^^^^^^ occurs within macro expansion
...
795 |                     handle_extend!(new_array, handle, upto, self.memory.array_len(handle.clone()), false, true);
    |                     ------------------------------------------------------------------------------------------- in this macro invocation
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
    = note: this error originates in the macro `handle_extend` (in Nightly builds, run with -Z macro-backtrace for more info)
help: make the implied reference pattern explicit
    |
763 |                                 let &mut MemCell::Array(_, ref mut v) = self.memory.cell_mut($to.clone()) else { unreachable!(); };
    |                                     ++++

error: binding modifiers may only be written when the default binding mode is `move`
   --> src/memory.rs:800:29
    |
800 |                         let MemCell::Array(_, ref mut v) = self.memory.cell_mut(new_array) else { unreachable!(); };
    |                                               ^^^^^^^ binding modifier not allowed under `ref mut` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> src/memory.rs:800:11
    |
800 |                         let MemCell::Array(_, ref mut v) = self.memory.cell_mut(new_array) else { unreachable!(); };
    |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this matches on type `&mut _`
help: remove the unnecessary binding modifier
    |
800 -                         let MemCell::Array(_, ref mut v) = self.memory.cell_mut(new_array) else { unreachable!(); };
800 +                         let MemCell::Array(_, v) = self.memory.cell_mut(new_array) else { unreachable!(); };
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> src/memory.rs:804:29
    |
804 |                         let MemCell::Array(_, ref mut v) = self.memory.cell_mut(handle) else { unreachable!(); };
    |                                               ^^^^^^^ binding modifier not allowed under `ref mut` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> src/memory.rs:804:11
    |
804 |                         let MemCell::Array(_, ref mut v) = self.memory.cell_mut(handle) else { unreachable!(); };
    |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this matches on type `&mut _`
help: remove the unnecessary binding modifier
    |
804 -                         let MemCell::Array(_, ref mut v) = self.memory.cell_mut(handle) else { unreachable!(); };
804 +                         let MemCell::Array(_, v) = self.memory.cell_mut(handle) else { unreachable!(); };
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> src/reader.rs:452:71
    |
452 |                             let Some(StackFrame{node:AstNode {content:AstContent::Group(ref mut v),..},..}) = stack.last_mut() else {
    |                                                                                         ^^^^^^^ binding modifier not allowed under `ref mut` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> src/reader.rs:452:20
    |
452 |                             let Some(StackFrame{node:AstNode {content:AstContent::Group(ref mut v),..},..}) = stack.last_mut() else {
    |                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this matches on type `&mut _`
help: remove the unnecessary binding modifier
    |
452 -                             let Some(StackFrame{node:AstNode {content:AstContent::Group(ref mut v),..},..}) = stack.last_mut() else {
452 +                             let Some(StackFrame{node:AstNode {content:AstContent::Group(v),..},..}) = stack.last_mut() else {
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> src/reader.rs:646:69
    |
646 |                             let Some(StackFrame{node:AstNode {content:AstContent::Int(ref mut i),..},..}) = stack.last_mut() else { die(); };
    |                                                                                       ^^^^^^^ binding modifier not allowed under `ref mut` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> src/reader.rs:646:20
    |
646 |                             let Some(StackFrame{node:AstNode {content:AstContent::Int(ref mut i),..},..}) = stack.last_mut() else { die(); };
    |                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this matches on type `&mut _`
help: remove the unnecessary binding modifier
    |
646 -                             let Some(StackFrame{node:AstNode {content:AstContent::Int(ref mut i),..},..}) = stack.last_mut() else { die(); };
646 +                             let Some(StackFrame{node:AstNode {content:AstContent::Int(i),..},..}) = stack.last_mut() else { die(); };
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> src/reader.rs:661:71
    |
661 |                         let Some(StackFrame{node:AstNode {content:AstContent::String(ref mut s),..},..}) = stack.last_mut() else { die(); };
    |                                                                                      ^^^^^^^ binding modifier not allowed under `ref mut` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> src/reader.rs:661:19
    |
661 |                         let Some(StackFrame{node:AstNode {content:AstContent::String(ref mut s),..},..}) = stack.last_mut() else { die(); };
    |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this matches on type `&mut _`
help: remove the unnecessary binding modifier
    |
661 -                         let Some(StackFrame{node:AstNode {content:AstContent::String(ref mut s),..},..}) = stack.last_mut() else { die(); };
661 +                         let Some(StackFrame{node:AstNode {content:AstContent::String(s),..},..}) = stack.last_mut() else { die(); };
    |

error: binding modifiers may only be written when the default binding mode is `move`
   --> src/reader.rs:726:71
    |
726 |                         let Some(StackFrame{node:AstNode {content:AstContent::String(ref mut s),..},..}) = stack.last_mut() else { die(); };
    |                                                                                      ^^^^^^^ binding modifier not allowed under `ref mut` default binding mode
    |
    = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
note: matching on a reference type with a non-reference pattern changes the default binding mode
   --> src/reader.rs:726:19
    |
726 |                         let Some(StackFrame{node:AstNode {content:AstContent::String(ref mut s),..},..}) = stack.last_mut() else { die(); };
    |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this matches on type `&mut _`
help: remove the unnecessary binding modifier
    |
726 -                         let Some(StackFrame{node:AstNode {content:AstContent::String(ref mut s),..},..}) = stack.last_mut() else { die(); };
726 +                         let Some(StackFrame{node:AstNode {content:AstContent::String(s),..},..}) = stack.last_mut() else { die(); };
    |

error: could not compile `experimental-lisp-0` (lib) due to 9 previous errors

Desired output

Any output which allows me to understand what the error was. Although (see commit 3) I was able to change the code to compile, I still do not understand what it was I did that fixed it. The message "binding modifiers may only be written when the default binding mode is move" did not guide me, and honestly was even a little misleading.

Two specific recommended changes:

  • The errors in the above scroll link the "nightly" version of the edition guide. Because the error is describing a behavior of the stable compiler, and I am running the stable compiler, it should probably instead link the stable version of the same page.
  • On some of the errors above, there was a help note saying to add &mut in some place or other, and on some of the errors above, there was a help note saying to remove the "unnecessary binding modifier" from ref mut v. In fact, the solution in every case was to add &mut, and in no case that I tested did removing the ref/mut modifiers from ref mut v produce compiling code— for reference here is the exact diff between commits 2 and 3:
diff --git a/src/memory.rs b/src/memory.rs
index 5654bc9..50d3cfe 100644
--- a/src/memory.rs
+++ b/src/memory.rs
@@ -760,7 +760,7 @@ impl<'a> eval::Eval<'a> {
 							};
 							{
 								// Dump contents of temp into "to" handle
-								let &mut MemCell::Array(_, ref mut v) = self.memory.cell_mut($to.clone()) else { unreachable!(); };
+								let MemCell::Array(_, ref mut v) = self.memory.cell_mut($to.clone()) else { unreachable!(); };
 								v.extend(&temp);
 								temp.clear(); // Done with temp contents
 								if $update_to { // Update that the array is cleared until THE VALUE AFTER to_idx
@@ -797,11 +797,11 @@ impl<'a> eval::Eval<'a> {
 					// We now need to overwrite handle with new_array, but borrow rules make it awkward to see both at once
 					let mut temp: Vec<MemAddr> = Default::default(); // Don't just clear temp, dealloc its storage
 					{
-						let &mut MemCell::Array(_, ref mut v) = self.memory.cell_mut(new_array) else { unreachable!(); };
+						let MemCell::Array(_, ref mut v) = self.memory.cell_mut(new_array) else { unreachable!(); };
 						std::mem::swap(&mut temp, v);
 					}
 					{
-						let &mut MemCell::Array(_, ref mut v) = self.memory.cell_mut(handle) else { unreachable!(); };
+						let MemCell::Array(_, ref mut v) = self.memory.cell_mut(handle) else { unreachable!(); };
 						std::mem::swap(&mut temp, v);
 					}
 				} else {
diff --git a/src/reader.rs b/src/reader.rs
index e6c520d..f7d0ac8 100644
--- a/src/reader.rs
+++ b/src/reader.rs
@@ -449,7 +449,7 @@ pub fn ast<T: std::io::Read>(mut chars: char_reader::CharReader<T>, tag:String,
 
 			    	if is_dot {
 			    		if !lisp {
-					    	let Some(&mut StackFrame{node:AstNode {content:AstContent::Group(ref mut v),..},..}) = stack.last_mut() else {
+					    	let Some(StackFrame{node:AstNode {content:AstContent::Group(ref mut v),..},..}) = stack.last_mut() else {
 					    		return Err(Error {at, tag, message:"Found . in a confusing place; must follow a symbol or expression".to_string()});
 					    	};
 							let Some(container) = v.pop() else {
@@ -643,8 +643,7 @@ pub fn ast<T: std::io::Read>(mut chars: char_reader::CharReader<T>, tag:String,
 				    			ch)})  // TODO: Sanitize ch printout
 				    		}
 
-					    	let Some(&mut StackFrame{node:AstNode {content:AstContent::Int(ref mut i),..},..}) = stack.last_mut() else { die(); };
-
+					    	let Some(StackFrame{node:AstNode {content:AstContent::Int(ref mut i),..},..}) = stack.last_mut() else { die(); };
 					    	*i = if let Some(i2) = (*i).checked_mul(base_num)
 					    	     && let Some(i3) = i2.checked_add(
 					    	         if !is_neg { digit } else { -digit }
@@ -659,7 +658,7 @@ pub fn ast<T: std::io::Read>(mut chars: char_reader::CharReader<T>, tag:String,
 			    			return Err(Error {at, tag, message:format!("Illegal character for identifier: {}", ch)})  // TODO: Sanitize ch printout
 				    	}
 
-				    	let Some(&mut StackFrame{node:AstNode {content:AstContent::String(ref mut s),..},..}) = stack.last_mut() else { die(); };
+				    	let Some(StackFrame{node:AstNode {content:AstContent::String(ref mut s),..},..}) = stack.last_mut() else { die(); };
 				    	s.push(ch);
 			    	}
 			    },
@@ -724,7 +723,7 @@ pub fn ast<T: std::io::Read>(mut chars: char_reader::CharReader<T>, tag:String,
 				    }
 
 				    if let Some(ch2) = append {
-				    	let Some(&mut StackFrame{node:AstNode {content:AstContent::String(ref mut s),..},..}) = stack.last_mut() else { die(); };
+				    	let Some(StackFrame{node:AstNode {content:AstContent::String(ref mut s),..},..}) = stack.last_mut() else { die(); };
 			    		s.push(ch2);
 				    }
 			    },

…for this reason I suggest all the error messages should have suggested adding &mut, not just some of them.

Rationale and extra context

Some bits of context:

  • I have never really understood what placing & or ref into a pattern-match destructure does (or rather, I know what they do, but I've always been confused which was which). I was already not well equipped to solve this except by trial and error.
  • The ten sites where the error drops are all doing fundamentally the same thing: Calling a method (such as vec.last_mut) which returns a &mut to a variant type; let matching on a particular expected variant type, and extracting a mutable reference to a field within the variant; and then changing the referenced field within the variant.

Various reasons I do not like this error:

  • The error seems to make reference to concepts, such as "default binding mode" and "move", which are meaningful to Rust's own compiler authors rather than Rust end users. Rust usually avoids this sin in error messages.
  • The error applies to code which built without an error in the 2021 edition. I think this means that Rust has a hightened responsibility to make this newly-minted error message clear.
  • I do not feel I understand, reading the error, what "binding modifiers" are. Does it refer to ref? to mut? Both? (The linked "edition" page seems to imply both.)
  • I do not understand what a "default binding modifier" is, either from the errors or from skimming (it's very dense) the linked "edition" page.
  • I do not understand what move, in backticks, means. I am aware of a move keyword modifier that can be placed on closures, and I am aware that in Rust when a let destructure consumes the right side of the = this is an example of what a compiler textbook would call "move semantics". It seems like a reference to "move" in this context implies we are talking about the compiler textbook definition, but putting it in backticks implies we're talking about the Rust keyword somehow, and that doesn't appear near the error site.
  • Most critically, the error seems in fact to point me away from the problem, rather than toward it. To wit, the message implies the problem is with my binding modifiers, whereas in fact the problem was that somewhere else I had matched the pattern in a (newly as of edition 2024?) incorrect way, which had the symptom of causing the ref mut to be incorrect for what Rust now concludes my left-side pattern to express. I don't know if this (the true root of my error) is actually detectable, but some things that would have helped me work it out anyway are:
    • The compiler giving me enough information to understand why it concluded this particular variable binding was ineligible for ref mut— "because this match expression is garble farble, this variable binding must use move semantics".
    • Talking through what I think is happening here: In this case, the right side of the expression returns a &mut, so I would have expected the left side of the expression was matching a &mut. But maybe in Edition 2024, matching a "written as if move type" destructure on the left with a &mut on the right causes Rust to automatically dereference the &mut. This would explain why Rust is making confusing references to "move", and why adding &mut (suppressing the auto-dereference) fixed it. If this is the true problem: Even if Rust doesn't know where my bug originated, Rust could detect whether suppressing the (surprising, and not present in older Rusts) auto-dereference would have made my illegal ref mut legal, and when suggesting adding &mut used more explicit verbiage (e.g. maybe a whole sentence like "Because this is a value rather than a reference match, the binding modifier(s) are not allowed: ref, mut"— I don't know what vocabulary is correct here, although I do know I dislike the word "implied" in the current text "make the implied reference pattern explicit" because apparently a reference is not implied in edition 2024, or else my ref mut would have been legal.)
    • Not recommending removing the ref mut, as they are in fact needed (removing them, again, seems to produce nonworking code).
  • One of the ten error sites underlines "ref mut" and says: "binding modifier not allowed under ref mut default binding mode". This sentence is the most confusing of all, as it seems to imply you are not allowed to use ref mut when you are using ref mut. That feels circular, like putting up a sign that says "signs prohibited".

Other cases

Rust Version

rustc 1.88.0 (6b00bc388 2025-06-23)
binary: rustc
commit-hash: 6b00bc3880198600130e1cf62b8f8a93494488cc
commit-date: 2025-06-23
host: x86_64-unknown-linux-gnu
release: 1.88.0
LLVM version: 20.1.5

Anything else?

For comparison, I tried cargo +nightly build --release with rustc 1.90.0-nightly (a84ab0c 2025-07-06) (x86_64-unknown-linux-gnu LLVM version: 20.1.7) and got what seems, eyeballing it, to be identical output.

The system on which I am running these tests is running Debian Unstable (trixie) but I do not believe this will impact anything.

Notes on my three commits up top: On line 646 of reader.rs, there is a difference between "commit 1" and commits 2 and 3. This change is irrelevant to the issues above and can be ignored (I added a && let on this line and this is why I switched from edition 2021 to 2024 in the first place). The reason I believe my &mut changes in commit 3 are the correct ones (despite still not totally understanding them) is because I have extensive regression tests (run ./develop/regression.py -a from root of any commit), so I strongly believe commit 3 is functionally identical to commit 1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-diagnosticsArea: Messages for errors, warnings, and lintsA-edition-2024Area: The 2024 editionA-patternsRelating to patterns and pattern matchingT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions