Hello everyone,
I'm a Rust developer with limited experience in large-scale projects but significant time working on complex codebases in the language. I deeply admire Rust's design, especially its ownership model that provides memory safety without GC and predictable performance. However, I've consistently encountered a recurring friction point: the distinction between Copy and non-Copy types, which introduces unnecessary constant cognitive load—even in everyday code.
This proposal does not aim to "break" Rust but to evolve it toward better ergonomics in future editions (e.g., a post-2026 edition or Rust 2.0), while keeping its core pillars intact: unique ownership, strict borrow checker, deterministic drop, and zero-cost abstractions. It builds on previous discussions in this forum, such as "Ownership: Move by default" (2019) Ownership: Move by default, which explicitly mentions the cognitive load caused by needing to know whether a type implements Copy to predict behavior in assignments and parameter passing.
The Problem: Two Contradictory Rules and Constant Cognitive Load
In current Rust, there are two contradictory semantic rules for let y = x; or passing x by value:
- For types implementing Copy (primitives like i32, small structs without heap/Drop):
xremains alive and usable afterward — it's an implicit bitwise copy. - For non-Copy types (String, Vec, most complex structs):
xbecomes invalid — it's a move of ownership.
This duality forces a constant "mental pause": in every assignment or call, you must recall or check if the type is Copy. In large projects (millions of lines, like Servo or Firefox), this accumulates cognitive fatigue, subtle errors, and noise during refactors. For example:
let a: i32 = 42;
let b = a; // a remains alive (rule 1)
println!("{}", a); // OK
let s: String = "hello".to_string();
let t = s; // s becomes invalid (rule 2)
println!("{}", s); // Error: use of moved value
Beginners get confused transitioning from primitives to heap types; experts waste time inspecting traits/docs in generics or legacy code. This subtly violates "zero-cost abstractions" by imposing unnecessary mental cost in 90% of trivial cases.
The Proposal: One Single Universal Rule — "Move Always Invalidates the Original"
Apply one single semantic rule for all types:
let y = x;or passingxby value is always a move: transfers ownership, invalidatesx. No exceptions based on type. The compiler optimizes moves of trivial types (no heap/Drop) to internal bitwise copies, but the visible semantics remain uniform.
Examples in this "homogeneous Rust":
let a: i32 = 42;
let b = a; // Move: a becomes invalid
println!("{}", a); // Error: use of moved value
let s: String = "hello".to_string();
let t = s; // Move: s becomes invalid
println!("{}", s); // Error: use of moved value
- For trivial types: Compiler uses NRVO/RVO to optimize to bitwise copy (identical performance to current Copy).
- For heap types: Transfers pointer/len/cap (O(1), like current move).
- No public Copy trait — behavior is automatic and predictable.
When you need to use the original afterward: Use explicit references (&x), which is already idiomatic for sharing.
Practical example (constant configuration):
let port: i32 = 8080;
setup_server(&port); // Immutable borrow: port remains alive
log_port(&port); // Another borrow
println!("{}", port); // OK
Key Advantages
- Drastic reduction in cognitive load: One single rule to learn and apply. No more "it depends on the type" — seamless flow in large code without mental pauses.
- Consistency and teachability: Rust becomes more intuitive for beginners (one rule from day 1) and experts (local predictable reasoning).
- Enhanced safety: Forces intentional thinking about ownership when sharing, reducing unintentional copies in structs that "used to be Copy but grew".
- Predictable performance: Defaults to O(1) moves. Existing optimizations (NRVO) intact — zero overhead.
- Compatibility: Implementable in a future edition (opt-in via flag, transpile legacy code by adding explicits where needed).
In large projects, 95% of assignments are temporary or final — you don't use the original afterward, so little code change. When you do, & is trivial.
Possible Criticisms and Anticipated Responses
-
Criticism: Loss of Ergonomics for Primitive Types Criticism: "Now I can't use an i32 after moving it without using &. This breaks the simplicity and ergonomics of primitive types." Anticipated Response: In most cases, primitive types like i32 are moved and not needed again afterward. When you do need to reference them multiple times, you simply use & (as in &i32). Ergonomics are not lost; they become consistent with the handling of other types. This change improves long-term predictability. Moreover, the cognitive load of remembering whether a type is Copy or not is completely eliminated. In real-world large projects, primitives are rarely used after a move in ways that require the original to remain alive without borrowing.
-
Criticism: Introduces Overhead from References Criticism: "Explicitly using & for primitive types becomes annoying and leads to unnecessary references." Anticipated Response: In practice, explicit references in Rust are lightweight and already idiomatic (e.g., &str or &[T]). Transitioning primitives to the same pattern creates uniformity across the language. The overall consistency gain in large codebases far outweighs the minor inconvenience of adding & in the few cases where sharing is needed. IDEs and the borrow checker already suggest these fixes automatically.
-
Criticism: Loss of Fine-Grained Control over Data Movement Criticism: "Rust provides very fine control over data movement with Copy and Move. This is lost with the uniform model." Anticipated Response: Fine-grained control over data movement is not lost—everything is still a move under the proposed model. The difference is that all types now follow the same rule, reducing confusion. When explicit control over references or sharing is needed, & and &mut remain available. Removing Copy does not reduce control; it makes the model more predictable and easier to reason about.
-
Criticism: Complications for Types with Drop or Dynamic Memory Criticism: "Moving types that implement Drop (like String, Vec, etc.) could become problematic since they would now move uniformly." Anticipated Response: This behavior already exists in Rust today for non-Copy types. Types with Drop simply transfer ownership during a move, ensuring no resource duplication. Memory safety is preserved exactly as before because unique ownership is maintained. There is no implicit copying of heap data—only ownership transfer.
-
Criticism: Potential Performance Impacts Criticism: "Always moving types could reduce performance, especially with large data structures." Anticipated Response: The model introduces no performance penalty. For trivial types (like i32), the compiler already optimizes moves using NRVO/RVO, eliminating any copy overhead—generated assembly remains identical. For complex types (String, Vec), moves transfer only pointers/length/capacity (O(1)), exactly as today. Performance remains predictable and optimal, with zero additional runtime cost.
-
Criticism: Backward Compatibility Breakage Criticism: "This proposal is not backward-compatible with current Rust and would break the ecosystem." Anticipated Response: The proposal is designed for a future Rust edition (post-2024 or Rust 2.0), allowing a smooth transition with a compatibility mode for legacy code. Migration could be gradual, with compiler flags or automatic fixes (e.g., inserting & where legacy code expects Copy behavior). Existing crates and FFI remain unaffected in compat mode.
-
Criticism: Breaks Rust's Ownership and Borrowing Model Criticism: "Rust's ownership and borrowing system is one of its greatest strengths. Doesn't this interfere with it?" Anticipated Response: The proposal does not modify the core ownership or borrowing model—it strengthens it by applying a single consistent rule for moves. Borrowing (& and &mut) works exactly as before. The change only simplifies type behavior during moves, removing exceptions and making ownership reasoning more intuitive without altering safety guarantees.
-
Criticism: Potential Complexity in Generics Criticism: "Introducing T: Movable for generics could add unnecessary complexity or surprises." Anticipated Response: Actually, this simplifies generics. The distinction between T: Copy and T: Clone disappears, reducing bound clutter. Most generic functions will no longer need explicit Copy or Clone bounds. The default Movable trait (applicable to all types) makes abstractions cleaner and more predictable.
-
Criticism: No Real Benefits Criticism: "Rust is already very consistent in memory safety and ownership—why change something that works well?" Anticipated Response: While Rust is excellent, the Copy/non-Copy distinction remains a persistent source of cognitive friction. Programmers constantly need to recall or check whether a type is Copy, even in trivial code. This proposal eliminates that mental overhead, significantly improving development flow in large projects. Consistency and predictability are enhanced without sacrificing any of Rust's core strengths.
-
Criticism: Breaks Zero-Cost Abstractions Criticism: "The transition cost and behavior changes could violate the principle of zero-cost abstractions." Anticipated Response: The model preserves zero-cost abstractions completely. Optimizations like NRVO and RVO remain fully applicable. The change removes cognitive cost (having to think about type categories on every move) while keeping runtime cost identical. In fact, the simplified model can improve code clarity and maintainability.
Conclusion
This proposal makes Rust more powerful by removing a real source of friction without altering its fundamental strengths. It is implementable via a compiler fork (with an MVP using macros for prototyping) and scalable to a formal RFC if there is interest.
What do you think? Have you experienced this cognitive load in large projects? Ideas for refinement?
Thanks for reading, jac