Summary
Maintain eliminate inlines a single-use let binding into a struct field initializer before a subsequent devlog! macro call that also references the same variable. Because eliminate counts the devlog! token-stream reference as a separate use and considers only the first use site, it incorrectly moves the value into the struct literal and then the devlog! line borrows the already-moved value, producing E0382.
Reproduction
~/Developer/Application/CodeEditorLand/Land/Target/release/Maintain eliminate \
--path ./Source/Air \
--verbose
Then cargo build (or cargo check) on Element/Mountain:
error[E0382]: borrow of moved value: `path`
--> Element/Mountain/Source/Air/AirClient.rs:631:7
|
611 | pub async fn get_file_info(&self, request_id:String, path:String) -> ...
| ---- move occurs because `path` has type `std::string::String`
...
623 | .get_file_info(Request::new(FileInfoRequest { request_id, path }))
| ---- value moved here
...
631 | path.clone(),
| ^^^^ value borrowed here after move
error[E0382]: borrow of moved value: `section`
--> Element/Mountain/Source/Air/AirClient.rs:949:7
|
928 | section:String,
| ------- move occurs because `section` has type `std::string::String`
...
941 | .get_configuration(Request::new(ConfigurationRequest { request_id, section }))
| ------- value moved here
...
949 | section.clone(),
| ^^^^^^^ value borrowed here after move
Original Code (before eliminate)
In get_file_info, the original pattern was:
let path_display = path.clone(); // used in devlog! below
let request = FileInfoRequest { request_id, path }; // path moved here
...
devlog!("...", path_display, path.clone(), ...);
path_display was a dedicated clone captured before the move. eliminate saw path_display as a single-use variable and inlined it, collapsing the pattern to:
// after eliminate:
.get_file_info(Request::new(FileInfoRequest { request_id, path }))
// path is now moved ^^ ...
devlog!("...", path.clone(), path.clone(), ...) // E0382: path already moved
The same pattern occurs in get_configuration with section_display / section.
Root Cause
Count::CountReferences counts identifiers in subsequent statements including inside macro token streams. When the pattern is:
let display = var.clone(); // (A) single-use candidate
// ... var is used (moved) later at (B)
devlog!(..., display, ...); // (C) — the only use of `display`
display genuinely has exactly one reference (at C), so eliminate inlines var.clone() at C and removes the let display line. But the reference count for var itself is computed separately. If var appears only in (B) from the perspective of the plain-expression counter (the struct field), but also appears later in a devlog! token stream at (C) via the original display name—after inlining, the devlog! now references var.clone() where var has already been moved at (B).
The core issue is that eliminate does not check whether inlining a clone() initializer into a later statement would leave the original variable (var) in a moved state at an earlier point in the same block. The substitution is order-aware for the candidate variable, but not for the variables inside the candidate's initializer expression.
Expected Behaviour
If inlining display = var.clone() at its use site would place a clone of var after a point where var has already been moved (or after it is consumed by another expression), the binding must be kept as-is. The safety check should verify that all free variables in the candidate's initializer expression remain valid (not moved) at every point after the candidate's original declaration site and up to and including the substitution site.
Suggested Fix
In Source/Eliminate/Transform/Safe.rs (or as an additional guard in Inline::EliminateBlock before calling SubstituteRef):
- Collect the set of free identifiers in
Candidate.Init.
- For each such identifier, scan the statements between
Candidate.StmtIndex and the substitution site (the use of the candidate) to check whether any of them move the free identifier (i.e., pass it by value to a function, use it in a struct literal field without .clone(), or assign it to another binding without a reference).
- If any free identifier in
Init would be in a moved state at the substitution site, skip inlining this candidate.
This is conservative but correct. A more precise version would use dataflow / borrow analysis, but a simple "does a statement between declaration and use consume this identifier by value" heuristic would catch the exact patterns above.
Affected Functions
AirClient::get_file_info — path_display = path.clone() inlined, path later moved into FileInfoRequest
AirClient::get_configuration — section_display = section.clone() inlined, section later moved into ConfigurationRequest
Likely affects any function where a display-style clone variable is declared before the original value is moved into a struct or function call.
Context
Summary
Maintain eliminateinlines a single-useletbinding into a struct field initializer before a subsequentdevlog!macro call that also references the same variable. Becauseeliminatecounts thedevlog!token-stream reference as a separate use and considers only the first use site, it incorrectly moves the value into the struct literal and then thedevlog!line borrows the already-moved value, producingE0382.Reproduction
~/Developer/Application/CodeEditorLand/Land/Target/release/Maintain eliminate \ --path ./Source/Air \ --verboseThen
cargo build(orcargo check) onElement/Mountain:Original Code (before eliminate)
In
get_file_info, the original pattern was:path_displaywas a dedicated clone captured before the move.eliminatesawpath_displayas a single-use variable and inlined it, collapsing the pattern to:The same pattern occurs in
get_configurationwithsection_display/section.Root Cause
Count::CountReferencescounts identifiers in subsequent statements including inside macro token streams. When the pattern is:displaygenuinely has exactly one reference (at C), soeliminateinlinesvar.clone()at C and removes thelet displayline. But the reference count forvaritself is computed separately. Ifvarappears only in (B) from the perspective of the plain-expression counter (the struct field), but also appears later in adevlog!token stream at (C) via the originaldisplayname—after inlining, thedevlog!now referencesvar.clone()wherevarhas already been moved at (B).The core issue is that
eliminatedoes not check whether inlining aclone()initializer into a later statement would leave the original variable (var) in a moved state at an earlier point in the same block. The substitution is order-aware for the candidate variable, but not for the variables inside the candidate's initializer expression.Expected Behaviour
If inlining
display = var.clone()at its use site would place a clone ofvarafter a point wherevarhas already been moved (or after it is consumed by another expression), the binding must be kept as-is. The safety check should verify that all free variables in the candidate's initializer expression remain valid (not moved) at every point after the candidate's original declaration site and up to and including the substitution site.Suggested Fix
In
Source/Eliminate/Transform/Safe.rs(or as an additional guard inInline::EliminateBlockbefore callingSubstituteRef):Candidate.Init.Candidate.StmtIndexand the substitution site (the use of the candidate) to check whether any of them move the free identifier (i.e., pass it by value to a function, use it in a struct literal field without.clone(), or assign it to another binding without a reference).Initwould be in a moved state at the substitution site, skip inlining this candidate.This is conservative but correct. A more precise version would use dataflow / borrow analysis, but a simple "does a statement between declaration and use consume this identifier by value" heuristic would catch the exact patterns above.
Affected Functions
AirClient::get_file_info—path_display = path.clone()inlined,pathlater moved intoFileInfoRequestAirClient::get_configuration—section_display = section.clone()inlined,sectionlater moved intoConfigurationRequestLikely affects any function where a
display-style clone variable is declared before the original value is moved into a struct or function call.Context
Maintain eliminate --path ./Source/Air --verboseCurrentStringdoes not implementCopy, so field shorthand syntax in struct literals ({ field }instead of{ field: field.clone() }) always moves the value