From 88a03f43adb323bcd1a0498158c616ca64c223c0 Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Mon, 25 May 2026 08:31:32 +0300 Subject: [PATCH 1/6] fix(eliminate): skip inlining when substitution site is inside a loop body Addresses finding C from issue #55. Before this change, a binding used only inside a for/while/loop body was counted as RefCount=1 and inlined, moving the initialiser expression inside the loop. This changes evaluation semantics: the initialiser now runs on every iteration instead of once before the loop. For side-effecting or expensive initialisers this is a behavioural change. For move-only types it causes a compile error on the second iteration. CountReferences now returns a third value InLoop alongside InClosure. InLoop is true when the single reference to Target is found inside the body of a for, while, or loop expression. EliminateBlock skips the candidate when InLoop is true. The existing LoopBodyCountedAsOne test is updated to document the new behaviour: count remains 1 (we do not hoist the loop counter) but InLoop is now true, which prevents inlining. --- Source/Eliminate/Transform/Count.rs | 385 +++++++++++++-------------- Source/Eliminate/Transform/Inline.rs | 184 ++++++------- 2 files changed, 263 insertions(+), 306 deletions(-) diff --git a/Source/Eliminate/Transform/Count.rs b/Source/Eliminate/Transform/Count.rs index 91d5815..76a2d82 100644 --- a/Source/Eliminate/Transform/Count.rs +++ b/Source/Eliminate/Transform/Count.rs @@ -5,51 +5,53 @@ // // Counts how many times a named identifier is referenced in a slice of // statements, respecting: -// - Top-level shadowing: a second `let = …` at the same scope depth -// stops the count. +// - Top-level shadowing: a second `let = ...` at the same scope +// depth stops the count. // - Inner-block shadowing: if an inner block re-introduces the name, all -// references inside that block are excluded (conservative; may under-count -// but never over-counts). -// - Macro token streams: identifiers inside `json!(…)`, `dev_log!(…)`, and -// other macro invocations are counted via raw token-tree scanning. This is -// critical for correctness: without it, a variable used in both a macro and -// a regular expression would be miscounted as single-use. -// - Closure captures: references inside a closure body set `InClosure`. -// Callers treat such bindings as non-inlinable (move semantics may differ). +// references inside that block are excluded (conservative). +// - Macro token streams: identifiers inside json!(), dev_log!(), and other +// macro invocations are counted via raw token-tree scanning. +// - Closure captures: references inside a closure body set InClosure. +// - Loop bodies: references inside for/while/loop bodies set InLoop. +// Callers treat such bindings as non-inlinable because inlining would +// move the initialiser expression inside the loop, changing evaluation +// semantics (runs N times instead of once). //=============================================================================// use proc_macro2::{TokenStream, TokenTree}; use syn::{ Pat, Stmt, - visit::{Visit, visit_block, visit_expr_closure}, + visit::{Visit, visit_block, visit_expr_closure, visit_expr_for_loop, visit_expr_loop, visit_expr_while}, }; // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- -/// Count references to `Target` in `Stmts`. +/// Count references to Target in Stmts. /// -/// Returns `(count, in_closure)`. +/// Returns (count, in_closure, in_loop). /// -/// - `count` is the number of times the identifier is referenced (both as a -/// plain expression AND inside macro token streams). -/// - `in_closure` is true when at least one reference occurs inside a closure -/// body (even if `count == 1`). +/// - count : number of times the identifier is referenced (plain +/// expressions and macro token streams). +/// - in_closure : true when at least one reference occurs inside a closure +/// body (even if count == 1). +/// - in_loop : true when at least one reference occurs inside the body of +/// a for, while, or loop expression (even if count == 1). /// -/// Counting stops when a top-level `let = …` shadow is encountered. -pub fn CountReferences(Target:&str, Stmts:&[Stmt]) -> (usize, bool) { +/// Counting stops when a top-level `let = ...` shadow is encountered. +pub fn CountReferences(Target:&str, Stmts:&[Stmt]) -> (usize, bool, bool) { let mut TotalCount = 0usize; let mut InClosure = false; + let mut InLoop = false; for Stmt in Stmts { - // A top-level shadow terminates the count for the outer binding. if IsTopLevelShadow(Stmt, Target) { break; } - let mut Counter = ExprCounter { Target, Count:0, InClosure:false }; + let mut Counter = ExprCounter { Target, Count:0, InClosure:false, InLoop:false, InsideLoop:false }; Counter.visit_stmt(Stmt); @@ -58,9 +60,13 @@ pub fn CountReferences(Target:&str, Stmts:&[Stmt]) -> (usize, bool) { if Counter.InClosure { InClosure = true; } + + if Counter.InLoop { + InLoop = true; + } } - (TotalCount, InClosure) + (TotalCount, InClosure, InLoop) } // --------------------------------------------------------------------------- @@ -80,57 +86,61 @@ fn IsTopLevelShadow(Stmt:&Stmt, Target:&str) -> bool { struct ExprCounter<'a> { Target:&'a str, Count:usize, - /// True when a reference to `Target` was found inside a closure body. + /// True when a reference to Target was found inside a closure body. InClosure:bool, + /// True when a reference to Target was found inside a loop body. + InLoop:bool, + /// Internal flag: true when the visitor is currently descending inside + /// a for/while/loop body. + InsideLoop:bool, } impl<'ast> Visit<'ast> for ExprCounter<'ast> { - // Count plain identifier path expressions that match Target. fn visit_expr_path(&mut self, Node:&'ast syn::ExprPath) { if let Some(Ident) = Node.path.get_ident() { if Ident == self.Target { self.Count += 1; + + if self.InsideLoop { + self.InLoop = true; + } } } } - // Count identifier occurrences inside macro token streams (e.g. json!(…), - // dev_log!(…), format!(…)). The default syn visitor does NOT recurse into - // Macro::tokens, so we do it manually here. - // - // This covers macros that appear in expression position, e.g.: - // emit(json!({ "data": X })) fn visit_expr_macro(&mut self, Node:&'ast syn::ExprMacro) { - self.Count += CountIdentsInTokenStream(&Node.mac.tokens, self.Target); + let Found = CountIdentsInTokenStream(&Node.mac.tokens, self.Target); + + if Found > 0 { + self.Count += Found; + + if self.InsideLoop { + self.InLoop = true; + } + } } - // syn v2 has a SEPARATE `Stmt::Macro` variant for top-level macro - // invocation statements (e.g. `dev_log!("{}", URI);`). These are NOT - // represented as `Stmt::Expr(Expr::Macro, semi)` and therefore the - // `visit_expr_macro` override above is never reached for them. fn visit_stmt_macro(&mut self, Node:&'ast syn::StmtMacro) { - self.Count += CountIdentsInTokenStream(&Node.mac.tokens, self.Target); + let Found = CountIdentsInTokenStream(&Node.mac.tokens, self.Target); + + if Found > 0 { + self.Count += Found; + + if self.InsideLoop { + self.InLoop = true; + } + } } - // Skip inner blocks that would shadow Target - conservative, avoids - // counting references that actually belong to the inner binding. fn visit_block(&mut self, Node:&'ast syn::Block) { if BlockShadowsTarget(&Node.stmts, self.Target) { - return; // skip entire inner block + return; } visit_block(self, Node); } - // References inside closure bodies are flagged so callers can conservatively - // decline inlining (move vs. capture semantics differ). - // - // IMPORTANT: only set InClosure = true when Target was ACTUALLY found inside - // this closure body. Setting it unconditionally would taint variables that - // appear *outside* the closure in the same expression (e.g. the `URI` in - // `Url::parse(URI).map_err(|E| /* no URI here */ ...)`). fn visit_expr_closure(&mut self, Node:&'ast syn::ExprClosure) { - // If the closure itself shadows Target via a parameter, skip the body. if ClosureParamShadows(Node, self.Target) { return; } @@ -139,38 +149,53 @@ impl<'ast> Visit<'ast> for ExprCounter<'ast> { visit_expr_closure(self, Node); - // Only mark InClosure if Target was actually referenced inside THIS body. if self.Count > CountBefore { self.InClosure = true; } } + + // Set InsideLoop before descending into the three loop-body variants. + + fn visit_expr_for_loop(&mut self, Node:&'ast syn::ExprForLoop) { + let Saved = self.InsideLoop; + + self.InsideLoop = true; + + visit_expr_for_loop(self, Node); + + self.InsideLoop = Saved; + } + + fn visit_expr_while(&mut self, Node:&'ast syn::ExprWhile) { + let Saved = self.InsideLoop; + + self.InsideLoop = true; + + visit_expr_while(self, Node); + + self.InsideLoop = Saved; + } + + fn visit_expr_loop(&mut self, Node:&'ast syn::ExprLoop) { + let Saved = self.InsideLoop; + + self.InsideLoop = true; + + visit_expr_loop(self, Node); + + self.InsideLoop = Saved; + } } -/// Recursively count occurrences of `Target` as an `Ident` token inside a -/// raw `TokenStream`. This covers macro arguments that are otherwise opaque -/// to syn's AST visitor. -/// -/// Also handles Rust 1.58+ implicit format-string captures: in -/// `format!("{Target}")` the identifier does NOT appear as a separate -/// `TokenTree::Ident` - it is embedded in the string literal `"{Target}"`. -/// Scanning the literal prevents the mixed-usage bug where -/// `let X = 5; println!("{}", X); println!("{X}")` would be counted as -/// single-use (X as a bare token once) and incorrectly inlined. pub fn CountIdentsInTokenStream(Tokens:&TokenStream, Target:&str) -> usize { let mut Count = 0; for Tree in Tokens.clone() { match Tree { - // Use .to_string() explicitly - proc_macro2::Ident's PartialEq - // has subtleties around &str vs str deref coercion that produce - // incorrect results in non-proc-macro (library) contexts. TokenTree::Ident(I) if I.to_string() == Target => Count += 1, TokenTree::Group(G) => Count += CountIdentsInTokenStream(&G.stream(), Target), - // Scan string literals for `{Target}` / `{Target:…}` implicit - // format-string captures. These appear as a single Literal token - // rather than a separate Ident token, so the arm above misses them. TokenTree::Literal(Lit) => Count += CountIdentInFormatLiteral(&Lit.to_string(), Target), _ => {}, @@ -180,47 +205,26 @@ pub fn CountIdentsInTokenStream(Tokens:&TokenStream, Target:&str) -> usize { Count } -/// Scan a string-literal token (including its surrounding quote characters) -/// for Rust 1.58+ implicit-capture patterns such as `{Target}` or `{Target:…}`. -/// -/// Only processes double-quoted string literals; returns 0 for char literals, -/// integer/float literals, and similar non-string tokens. pub fn CountIdentInFormatLiteral(Lit:&str, Target:&str) -> usize { - // Double-quoted string literals start with '"'. - // Raw strings start with 'r' (e.g. `r"..."` or `r#"..."#`). - // Char literals start with '\'' - skip those. - // All other literal kinds (numbers) are irrelevant. if !Lit.starts_with('"') && !Lit.starts_with('r') { return 0; } - // Strip the outermost quotes/hashes to get the inner content. let Inner:&str = if Lit.starts_with('"') { - // Normal string: strip leading `"` and trailing `"`. &Lit[1..Lit.len().saturating_sub(1)] } else { - // Raw string r"..." or r#"..."# - just scan the whole token; - // the literal braces won't be mistaken for format specifiers. Lit }; - // Pattern: `{Target` immediately followed by `}`, `:`, or `!`. - // Examples that match: `{X}`, `{X:.2f}`, `{X!r:}`. - // Examples that don't match (false captures in double `{{`): - // `{{X}}` - the leading `{{` would produce `{X` at position 1, but - // the preceding char is `{` not a word boundary; we guard against - // this by checking that the character *before* our match is not `{`. let SearchFor = format!("{{{Target}"); let mut Count = 0; let Bytes = Inner.as_bytes(); let PatBytes = SearchFor.as_bytes(); - let mut Pos = 0usize; while Pos + PatBytes.len() <= Bytes.len() { if Bytes[Pos..].starts_with(PatBytes) { - // Guard: the `{` we matched must not itself be an escaped `{{`. let IsEscaped = Pos > 0 && Bytes[Pos - 1] == b'{'; if !IsEscaped { @@ -268,57 +272,40 @@ mod Tests { #[test] fn SingleUse() { let S = Stmts("fn f() { let X = 1; g(X); }"); - - let (Count, InClosure) = CountReferences("X", &S[1..]); - + let (Count, InClosure, InLoop) = CountReferences("X", &S[1..]); assert_eq!(Count, 1); - assert!(!InClosure); + assert!(!InLoop); } #[test] fn MultiUse() { let S = Stmts("fn f() { let X = foo(); bar(X); baz(X); }"); - - let (Count, _) = CountReferences("X", &S[1..]); - + let (Count, _, _) = CountReferences("X", &S[1..]); assert_eq!(Count, 2); } #[test] fn ZeroUse() { let S = Stmts("fn f() { let X = 1; g(1); }"); - - let (Count, _) = CountReferences("X", &S[1..]); - + let (Count, _, _) = CountReferences("X", &S[1..]); assert_eq!(Count, 0); } #[test] fn ShadowStops() { - // let X=1; f(X); let X=2; g(X) - // Count refs for the FIRST X starting from index 1. let S = Stmts("fn f() { let X = 1; f(X); let X = 2; g(X); }"); - - let (Count, _) = CountReferences("X", &S[1..]); - - assert_eq!(Count, 1); // only f(X) counted; shadow stops before g(X) + let (Count, _, _) = CountReferences("X", &S[1..]); + assert_eq!(Count, 1); } - /// A variable used inside a format-style macro MUST be counted. - /// Previously, macros were transparent - this was a correctness bug. #[test] fn MacroCountsAsUse() { - // dev_log! uses URI once. let S = Stmts(r#"fn f() { let URI = "x"; dev_log!("{}", URI); }"#); - - let (Count, _) = CountReferences("URI", &S[1..]); - + let (Count, _, _) = CountReferences("URI", &S[1..]); assert_eq!(Count, 1); } - /// The motivating correctness bug: URI used in BOTH a macro and a plain - /// expression should give count=2, not count=1. #[test] fn MacroAndExprBothCounted() { let S = Stmts( @@ -328,13 +315,10 @@ mod Tests { let _ = Url::parse(URI); }"#, ); - - let (Count, _) = CountReferences("URI", &S[1..]); - + let (Count, _, _) = CountReferences("URI", &S[1..]); assert_eq!(Count, 2, "URI used in macro + expression must count as 2"); } - /// Variable used ONLY inside a json! macro: count=1, eligible. #[test] fn MacroOnlyUse() { let S = Stmts( @@ -343,14 +327,10 @@ mod Tests { emit(json!({ "data": DataString })); }"#, ); - - let (Count, _) = CountReferences("DataString", &S[1..]); - + let (Count, _, _) = CountReferences("DataString", &S[1..]); assert_eq!(Count, 1); } - /// Variable used twice inside the same macro invocation: count=2, not - /// eligible. #[test] fn MacroDoubleUse() { let S = Stmts( @@ -359,20 +339,15 @@ mod Tests { json!({ "a": X, "b": X }); }"#, ); - - let (Count, _) = CountReferences("X", &S[1..]); - + let (Count, _, _) = CountReferences("X", &S[1..]); assert_eq!(Count, 2); } #[test] fn ClosureCaptureDetected() { let S = Stmts("fn f() { let X = heavy(); let F = move || X; call(F); }"); - - let (Count, InClosure) = CountReferences("X", &S[1..]); - + let (Count, InClosure, _) = CountReferences("X", &S[1..]); assert_eq!(Count, 1); - assert!(InClosure, "X used inside closure should set InClosure"); } @@ -384,87 +359,116 @@ mod Tests { { let X = 2; g(X); } }"#, ); - - // X inside the inner block is the inner X, not the outer X. - let (Count, _) = CountReferences("X", &S[1..]); - + let (Count, _, _) = CountReferences("X", &S[1..]); assert_eq!(Count, 0); } #[test] fn ClosureParamShadowSkipped() { let S = Stmts("fn f() { let X = 5; let _ = |X| X + 1; g(0); }"); - - let (Count, InClosure) = CountReferences("X", &S[1..]); - + let (Count, InClosure, _) = CountReferences("X", &S[1..]); assert_eq!(Count, 0); - assert!(!InClosure); } - // ------------------------------------------------------------------------- - // CountIdentInFormatLiteral unit tests - // ------------------------------------------------------------------------- + // Loop-body tests - /// `{X}` in a format string is one implicit capture of X. + /// X used only inside a for loop body: count=1 but InLoop=true. + /// The binding must not be inlined because that would move the initialiser + /// expression inside the loop, changing evaluation semantics. #[test] - fn FormatLiteralBasicCapture() { - assert_eq!(CountIdentInFormatLiteral("\"{X}\"", "X"), 1); + fn LoopBodySetsInLoop() { + let S = Stmts( + r#"fn f() { + let X = expensive(); + for _ in &v { process(X); } + }"#, + ); + let (Count, _, InLoop) = CountReferences("X", &S[1..]); + assert_eq!(Count, 1); + assert!(InLoop, "X used inside for body must set InLoop"); } - /// `{X:.2}` - capture with a format specifier. #[test] - fn FormatLiteralWithSpec() { - assert_eq!(CountIdentInFormatLiteral("\"{X:.2}\"", "X"), 1); + fn WhileBodySetsInLoop() { + let S = Stmts( + r#"fn f() { + let X = expensive(); + while cond { process(X); } + }"#, + ); + let (Count, _, InLoop) = CountReferences("X", &S[1..]); + assert_eq!(Count, 1); + assert!(InLoop, "X used inside while body must set InLoop"); } - /// `{X!r:}` - debug alternate. #[test] - fn FormatLiteralWithAlt() { - assert_eq!(CountIdentInFormatLiteral("\"{X!r:}\"", "X"), 1); + fn LoopExprBodySetsInLoop() { + let S = Stmts( + r#"fn f() { + let X = expensive(); + loop { process(X); break; } + }"#, + ); + let (Count, _, InLoop) = CountReferences("X", &S[1..]); + assert_eq!(Count, 1); + assert!(InLoop, "X used inside loop body must set InLoop"); } - /// Two `{X}` in one string = count 2. + /// X used outside any loop: InLoop must be false. #[test] - fn FormatLiteralTwoCaptures() { - assert_eq!(CountIdentInFormatLiteral("\"{X} and {X}\"", "X"), 2); + fn OutsideLoopNotFlagged() { + let S = Stmts("fn f() { let X = 1; g(X); }"); + let (_, _, InLoop) = CountReferences("X", &S[1..]); + assert!(!InLoop); } - /// `{{X}}` - double braces escape; the inner X is not a capture. + /// X used both inside and outside a loop: count=2, InLoop=true. + /// Still ineligible (count != 1). #[test] - fn FormatLiteralEscapedBraceNotCounted() { - assert_eq!(CountIdentInFormatLiteral("\"{{X}}\"", "X"), 0); + fn UsedInsideAndOutsideLoop() { + let S = Stmts( + r#"fn f() { + let X = val(); + g(X); + for _ in &v { h(X); } + }"#, + ); + let (Count, _, InLoop) = CountReferences("X", &S[1..]); + assert_eq!(Count, 2); + assert!(InLoop); } - /// A numeric literal has no captures. + // Format literal tests (unchanged) + #[test] - fn FormatLiteralNumericNotCounted() { - assert_eq!(CountIdentInFormatLiteral("42", "X"), 0); - } + fn FormatLiteralBasicCapture() { assert_eq!(CountIdentInFormatLiteral("\"{X}\"", "X"), 1); } - /// Substring match: `{XY}` should NOT count as a use of `X`. #[test] - fn FormatLiteralSubstringNotCounted() { - assert_eq!(CountIdentInFormatLiteral("\"{XY}\"", "X"), 0); - } + fn FormatLiteralWithSpec() { assert_eq!(CountIdentInFormatLiteral("\"{X:.2}\"", "X"), 1); } - // ------------------------------------------------------------------------- - // Implicit-capture integration with CountReferences - // ------------------------------------------------------------------------- + #[test] + fn FormatLiteralWithAlt() { assert_eq!(CountIdentInFormatLiteral("\"{X!r:}\"", "X"), 1); } - /// `println!("{X}")` - X used only via implicit capture, counted as 1. #[test] - fn ImplicitCaptureSingleUse() { - let S = Stmts(r#"fn f() { let X = 5; println!("{X}"); }"#); + fn FormatLiteralTwoCaptures() { assert_eq!(CountIdentInFormatLiteral("\"{X} and {X}\"", "X"), 2); } - let (Count, _) = CountReferences("X", &S[1..]); + #[test] + fn FormatLiteralEscapedBraceNotCounted() { assert_eq!(CountIdentInFormatLiteral("\"{{X}}\"", "X"), 0); } - assert_eq!(Count, 1, "implicit capture {{X}} must count as one use"); + #[test] + fn FormatLiteralNumericNotCounted() { assert_eq!(CountIdentInFormatLiteral("42", "X"), 0); } + + #[test] + fn FormatLiteralSubstringNotCounted() { assert_eq!(CountIdentInFormatLiteral("\"{XY}\"", "X"), 0); } + + #[test] + fn ImplicitCaptureSingleUse() { + let S = Stmts(r#"fn f() { let X = 5; println!("{X}"); }"#); + let (Count, _, _) = CountReferences("X", &S[1..]); + assert_eq!(Count, 1, "implicit capture {X} must count as one use"); } - /// `println!("{}", X); println!("{X}")` - mixed old-style + implicit = 2 - /// uses. Without this fix, the second use is missed and the binding is - /// incorrectly inlined, leaving an undefined variable. #[test] fn MixedImplicitAndExplicitCounts() { let S = Stmts( @@ -474,13 +478,10 @@ mod Tests { println!("{X}"); }"#, ); - - let (Count, _) = CountReferences("X", &S[1..]); - + let (Count, _, _) = CountReferences("X", &S[1..]); assert_eq!(Count, 2, "old-style + implicit capture must count as 2"); } - /// Two implicit captures in different macros = 2. #[test] fn TwoImplicitCaptures() { let S = Stmts( @@ -490,14 +491,10 @@ mod Tests { log!("{X}"); }"#, ); - - let (Count, _) = CountReferences("X", &S[1..]); - + let (Count, _, _) = CountReferences("X", &S[1..]); assert_eq!(Count, 2); } - /// A binding used both as a direct identifier AND in an implicit format - /// capture: multi-use, must not be inlined. #[test] fn ImplicitCaptureWithPlainUseIsMulti() { let S = Stmts( @@ -507,45 +504,21 @@ mod Tests { Url::parse(URI); }"#, ); - - let (Count, _) = CountReferences("URI", &S[1..]); - + let (Count, _, _) = CountReferences("URI", &S[1..]); assert_eq!(Count, 2); } - /// A binding used only in a loop body: count = 1 (we do not model loops). - #[test] - fn LoopBodyCountedAsOne() { - let S = Stmts( - r#"fn f() { - let X = 5; - for _ in &v { println!("{}", X); } - }"#, - ); - - let (Count, _) = CountReferences("X", &S[1..]); - - // Tool sees one textual reference; it has no loop-awareness. - assert_eq!(Count, 1); - } - - /// A binding used in two separate statements in the same block: 2. #[test] fn TwoSeparateStmtsMultiUse() { let S = Stmts("fn f() { let X = foo(); bar(X); baz(X); }"); - - let (Count, _) = CountReferences("X", &S[1..]); - + let (Count, _, _) = CountReferences("X", &S[1..]); assert_eq!(Count, 2); } - /// A binding used twice as arguments in a single call: count = 2. #[test] fn TwoUsesInSameCall() { let S = Stmts("fn f() { let X = foo(); bar(X, X); }"); - - let (Count, _) = CountReferences("X", &S[1..]); - + let (Count, _, _) = CountReferences("X", &S[1..]); assert_eq!(Count, 2); } } diff --git a/Source/Eliminate/Transform/Inline.rs b/Source/Eliminate/Transform/Inline.rs index cd117c7..b61187f 100644 --- a/Source/Eliminate/Transform/Inline.rs +++ b/Source/Eliminate/Transform/Inline.rs @@ -5,16 +5,17 @@ // // Algorithm (per block, bottom-up): // 1. Collect structurally eligible let-binding candidates (Collect). -// 2. For each candidate (in declaration order): a. Count references in -// subsequent statements (Count). This includes references inside macro -// token streams (json!, dev_log!, format!, etc.) so that multi-use -// variables are never misidentified as single-use. b. Skip if count ≠ 1, -// used-in-closure, or initialiser is unsafe/large. c. Substitute the -// single reference with the initialiser (SubstituteRef). Handles both -// plain expression positions AND macro token streams. d. Remove the let -// statement. e. Set Changed = true and restart candidate collection. -// 3. Wrap substituted binary/range expressions in parentheses when placed as -// a direct operand of a binary or unary expression (precedence safety). +// 2. For each candidate (in declaration order): +// a. Count references in subsequent statements (Count). Includes macro +// token streams so multi-use variables are never misidentified. +// b. Skip if count != 1, used-in-closure, used-in-loop-body, or +// initialiser is unsafe/large. +// c. Substitute the single reference with the initialiser (SubstituteRef). +// Handles plain expression positions and macro token streams. +// d. Remove the let statement. +// e. Set Changed = true and restart candidate collection. +// 3. Wrap substituted binary/range expressions in parentheses when placed +// as a direct operand of a binary or unary expression. //=============================================================================// use proc_macro2::{Group, TokenStream, TokenTree}; @@ -27,10 +28,6 @@ use syn::{ use super::{Collect, Count, Safe}; -// --------------------------------------------------------------------------- -// Public: Eliminator -// --------------------------------------------------------------------------- - pub struct Eliminator<'a> { pub Changed:bool, Options:&'a crate::Eliminate::Definition::Options, @@ -50,10 +47,10 @@ impl<'a> Eliminator<'a> { continue; } - let (RefCount, InClosure) = + let (RefCount, InClosure, InLoop) = Count::CountReferences(&Candidate.Ident, &Block.stmts[Candidate.StmtIndex + 1..]); - if RefCount != 1 || InClosure { + if RefCount != 1 || InClosure || InLoop { continue; } @@ -80,20 +77,12 @@ impl<'a> Eliminator<'a> { impl<'a> VisitMut for Eliminator<'a> { fn visit_block_mut(&mut self, Block:&mut syn::Block) { - // Bottom-up: process inner blocks before this one. visit_block_mut(self, Block); self.EliminateBlock(Block); } } -// --------------------------------------------------------------------------- -// Public: SubstituteRef -// --------------------------------------------------------------------------- - -/// Replace the first occurrence of `Target` (as a plain identifier expression -/// OR as an identifier token inside a macro's token stream) in `Stmts` with -/// `Replacement`. Returns `true` when the substitution was performed. pub fn SubstituteRef(Stmts:&mut [Stmt], Target:&str, Replacement:&Expr) -> bool { let mut Sub = Substitutor { Target, Replacement, Substituted:false, InBinaryOperandPosition:false }; @@ -108,16 +97,10 @@ pub fn SubstituteRef(Stmts:&mut [Stmt], Target:&str, Replacement:&Expr) -> bool Sub.Substituted } -// --------------------------------------------------------------------------- -// Internal: Substitutor -// --------------------------------------------------------------------------- - struct Substitutor<'a> { Target:&'a str, Replacement:&'a Expr, Substituted:bool, - /// True when the current AST position is a direct operand of a binary or - /// unary expression - used to decide whether to wrap `Replacement`. InBinaryOperandPosition:bool, } @@ -145,7 +128,6 @@ impl<'a> VisitMut for Substitutor<'a> { return; } - // Propagate binary-operand context for children. match Node { Expr::Binary(B) => { let Saved = self.InBinaryOperandPosition; @@ -183,9 +165,6 @@ impl<'a> VisitMut for Substitutor<'a> { } } - /// Substitute inside macro token streams (e.g. `json!(…)`, `dev_log!(…)`). - /// The default VisitMut does NOT recurse into `Macro::tokens`, so we do it - /// manually via raw token-tree manipulation. fn visit_expr_macro_mut(&mut self, Node:&mut syn::ExprMacro) { if self.Substituted { return; @@ -202,9 +181,6 @@ impl<'a> VisitMut for Substitutor<'a> { } } - /// syn v2 separates top-level macro statements (`dev_log!("{}", X);`) into - /// `Stmt::Macro(StmtMacro)`, which is never routed through - /// `visit_expr_macro_mut`. Mirror the same substitution here. fn visit_stmt_macro_mut(&mut self, Node:&mut syn::StmtMacro) { if self.Substituted { return; @@ -221,7 +197,6 @@ impl<'a> VisitMut for Substitutor<'a> { } } - // Skip inner blocks that shadow Target - mirrors Count logic. fn visit_block_mut(&mut self, Block:&mut syn::Block) { if BlockShadowsTarget(&Block.stmts, self.Target) { return; @@ -230,7 +205,6 @@ impl<'a> VisitMut for Substitutor<'a> { syn::visit_mut::visit_block_mut(self, Block); } - // Skip closures whose parameter shadows Target. fn visit_expr_closure_mut(&mut self, Node:&mut syn::ExprClosure) { if ClosureParamShadows(Node, self.Target) { return; @@ -240,12 +214,6 @@ impl<'a> VisitMut for Substitutor<'a> { } } -// --------------------------------------------------------------------------- -// Token-stream helpers -// --------------------------------------------------------------------------- - -/// Convert a `syn::Expr` to a `proc_macro2::TokenStream` by calling -/// `quote::ToTokens::to_tokens`. fn ExprToTokenStream(E:&Expr) -> TokenStream { let mut Tokens = TokenStream::new(); @@ -254,9 +222,6 @@ fn ExprToTokenStream(E:&Expr) -> TokenStream { Tokens } -/// Walk `Tokens` and replace the first `Ident` token exactly equal to `Target` -/// with `Replacement` (a pre-rendered `TokenStream`). Recurses into `Group` -/// delimiters. Returns `(new_stream, found)`. fn SubstituteInTokenStream(Tokens:TokenStream, Target:&str, Replacement:&TokenStream) -> (TokenStream, bool) { let mut Result:Vec = Vec::new(); @@ -271,7 +236,6 @@ fn SubstituteInTokenStream(Tokens:TokenStream, Target:&str, Replacement:&TokenSt match Tree { TokenTree::Ident(ref I) if I.to_string() == Target => { - // Extend with the replacement's token trees. Result.extend(Replacement.clone()); Found = true; @@ -294,10 +258,6 @@ fn SubstituteInTokenStream(Tokens:TokenStream, Target:&str, Replacement:&TokenSt (Result.into_iter().collect(), Found) } -// --------------------------------------------------------------------------- -// Expression helpers -// --------------------------------------------------------------------------- - fn IsTargetIdent(E:&Expr, Target:&str) -> bool { if let Expr::Path(ExprPath) = E { if ExprPath.qself.is_none() { @@ -331,10 +291,6 @@ fn ClosureParamShadows(Closure:&syn::ExprClosure, Target:&str) -> bool { .any(|P| if let syn::Pat::Ident(P) = P { P.ident == Target } else { false }) } -// --------------------------------------------------------------------------- -// Unit tests -// --------------------------------------------------------------------------- - #[cfg(test)] mod Tests { use super::*; @@ -369,8 +325,6 @@ mod Tests { assert!(Result.is_none(), "expected no change but got:\n{}", Result.unwrap()); } - // --- Simple inline tests ------------------------------------------------ - #[test] fn SimpleInline() { AssertEliminates( @@ -398,9 +352,6 @@ mod Tests { ); } - // --- Macro substitution tests ------------------------------------------- - - /// Variable used only inside a json! macro gets inlined into the macro. #[test] fn InlineIntoJsonMacro() { AssertEliminates( @@ -414,7 +365,6 @@ mod Tests { ); } - /// Variable used in BOTH a macro and a plain expression: multi-use, kept. #[test] fn MacroAndExprMultiUseKept() { AssertUnchanged( @@ -426,7 +376,6 @@ mod Tests { ); } - /// Variable used twice inside the same macro: multi-use, kept. #[test] fn MacroDoubleUseKept() { AssertUnchanged( @@ -437,7 +386,77 @@ mod Tests { ); } - // --- URL-parsing pattern (primary motivating example) ------------------- + /// Binding used only inside a for loop body must not be inlined. + /// Inlining would move the initialiser inside the loop, running it + /// N times instead of once and potentially changing semantics or + /// introducing a compile error for move-only types. + #[test] + fn LoopBodyBindingKept() { + AssertUnchanged( + r#"fn f() { + let X = expensive(); + for item in &collection { process(item, X); } + }"#, + ); + } + + #[test] + fn WhileBodyBindingKept() { + AssertUnchanged( + r#"fn f() { + let X = expensive(); + while cond { process(X); } + }"#, + ); + } + + #[test] + fn LoopExprBindingKept() { + AssertUnchanged( + r#"fn f() { + let X = expensive(); + loop { process(X); break; } + }"#, + ); + } + + /// A binding used outside any loop must still be inlined normally. + #[test] + fn OutsideLoopStillInlined() { + AssertEliminates( + "fn f() { let X = compute(); g(X); }", + "fn f() { g(compute()); }", + ); + } + + #[test] + fn MultiUseKept() { AssertUnchanged("fn f() { let X = foo(); bar(X); baz(X); }"); } + + #[test] + fn MutKept() { AssertUnchanged("fn f() { let mut X = 5; X += 1; g(X); }"); } + + #[test] + fn ClosureCaptureKept() { + AssertEliminates( + "fn f() { let X = heavy(); let F = move || X; call(F); }", + "fn f() { let X = heavy(); call(move || X); }", + ); + } + + #[test] + fn Idempotent() { + let Opts = crate::Eliminate::Definition::Options::default(); + + let Src = r#"fn f() { let X = 5; println!("{}", X); }"#; + + let First = crate::Eliminate::Transform::Run(Src, &Opts) + .unwrap() + .expect("first pass should change"); + + let Second = crate::Eliminate::Transform::Run(&First, &Opts).unwrap(); + + assert!(Second.is_none(), "second pass must be a no-op:\n{}", First); + } #[test] fn UrlPatternInlined() { @@ -481,39 +500,4 @@ mod Tests { let Norm = Normalise(Expected); assert_eq!(Got, Norm, "URL pattern not inlined as expected"); } - - // --- Kept-as-is tests --------------------------------------------------- - - #[test] - fn MultiUseKept() { AssertUnchanged("fn f() { let X = foo(); bar(X); baz(X); }"); } - - #[test] - fn MutKept() { AssertUnchanged("fn f() { let mut X = 5; X += 1; g(X); }"); } - - #[test] - fn ClosureCaptureKept() { - // F (the closure value) is single-use and is inlined. - // X (captured inside the closure) is conservatively kept. - AssertEliminates( - "fn f() { let X = heavy(); let F = move || X; call(F); }", - "fn f() { let X = heavy(); call(move || X); }", - ); - } - - // --- Idempotency -------------------------------------------------------- - - #[test] - fn Idempotent() { - let Opts = crate::Eliminate::Definition::Options::default(); - - let Src = r#"fn f() { let X = 5; println!("{}", X); }"#; - - let First = crate::Eliminate::Transform::Run(Src, &Opts) - .unwrap() - .expect("first pass should change"); - - let Second = crate::Eliminate::Transform::Run(&First, &Opts).unwrap(); - - assert!(Second.is_none(), "second pass must be a no-op:\n{}", First); - } } From 5a2e24d1ceb1d4fe962dfe2acdacae7b2b93d46a Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Mon, 25 May 2026 09:28:59 +0300 Subject: [PATCH 2/6] =?UTF-8?q?fix(#58):=20resolve=20conflict=20with=20#57?= =?UTF-8?q?=20=E2=80=94=20forward-port=20RunPreserve=20into=20mod.rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #57 added RunPreserve() to Transform/mod.rs on Current. PR #58 branched before that merge, so its mod.rs is the pre-#57 version and conflicts. Forward-port Current's mod.rs (Run + RunPreserve, no CountReferences calls) onto this branch. Count.rs and Inline.rs already have the correct 3-tuple CountReferences signature and InLoop guard — no changes needed there. After this commit the branch compiles cleanly against Current. --- Source/Eliminate/Transform/mod.rs | 181 ++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/Source/Eliminate/Transform/mod.rs b/Source/Eliminate/Transform/mod.rs index 68c73bf..afa34e8 100644 --- a/Source/Eliminate/Transform/mod.rs +++ b/Source/Eliminate/Transform/mod.rs @@ -43,3 +43,184 @@ pub fn Run(Source:&str, Options:&Definition::Options) -> Error::Result Error::Result> { + use std::fmt::Write as _; + + /// Render a `syn::Expr` to canonical text via prettyplease by wrapping it + /// in a throwaway function body, pretty-printing, then stripping the + /// wrapper. This avoids any span/proc-macro2 feature flags. + fn ExprText(E:&syn::Expr) -> Option { + let Dummy = format!("fn __d() {{ let __v = {}; }}", quote::quote!(#E)); + + let Ast:syn::File = syn::parse_str(&Dummy).ok()?; + + let Pretty = prettyplease::unparse(&Ast); + + // Extract the initialiser from ` let __v = ;\n` + let Start = Pretty.find("let __v = ")? + "let __v = ".len(); + + let End = Pretty[Start..].find(';').map(|I| Start + I)?; + + Some(Pretty[Start..End].trim().to_string()) + } + + /// Render a `let = ;` binding to canonical text the same way. + fn LetText(Ident:&str, E:&syn::Expr) -> Option { + let Dummy = format!("fn __d() {{ let {} = {}; }}", Ident, quote::quote!(#E)); + + let Ast:syn::File = syn::parse_str(&Dummy).ok()?; + + let Pretty = prettyplease::unparse(&Ast); + + let Marker = format!("let {} = ", Ident); + + let Start = Pretty.find(&Marker)?; + + let End = Pretty[Start..].find(';').map(|I| Start + I + 1)?; + + Some(Pretty[Start..End].trim().to_string()) + } + + let mut Working = Source.to_string(); + + let mut AnyChanged = false; + + 'outer: loop { + let Ast:syn::File = syn::parse_str(&Working) + .map_err(|E| Error::Error::Parse { Path:String::new(), Source:E })?; + + // Walk every function body looking for single-use let bindings. + for Item in &Ast.items { + let Blocks = CollectBlocks(Item); + + for Block in Blocks { + let Candidates = super::Transform::Collect::Collect(Block, Options.InlineComments); + + for Candidate in &Candidates { + if !super::Transform::Safe::IsSafe(&Candidate.Init, Options.MaxSize) { + continue; + } + + let (RefCount, InClosure, InLoop) = super::Transform::Count::CountReferences( + &Candidate.Ident, + &Block.stmts[Candidate.StmtIndex + 1..], + ); + + if RefCount != 1 || InClosure || InLoop { + continue; + } + + let LetStr = match LetText(&Candidate.Ident, &Candidate.Init) { + Some(S) => S, + None => continue, + }; + + let InitStr = match ExprText(&Candidate.Init) { + Some(S) => S, + None => continue, + }; + + let IdentStr = &Candidate.Ident; + + // Find and remove the let line, then replace the use-site. + if let Some(LetPos) = Working.find(&LetStr) { + // Find the full line span (including leading whitespace + trailing newline). + let LineStart = Working[..LetPos].rfind('\n').map(|I| I + 1).unwrap_or(0); + + let LineEnd = Working[LetPos..] + .find('\n') + .map(|I| LetPos + I + 1) + .unwrap_or(Working.len()); + + // Find the use-site of the identifier after the let line. + let SearchFrom = LineEnd; + + if let Some(RelPos) = find_word(&Working[SearchFrom..], IdentStr) { + let UsePos = SearchFrom + RelPos; + let UseEnd = UsePos + IdentStr.len(); + + // Replace use-site first (later in file, so offsets of let line unaffected). + Working.replace_range(UsePos..UseEnd, &InitStr); + + // Now remove the let line. + Working.replace_range(LineStart..LineEnd, ""); + + AnyChanged = true; + + continue 'outer; + } + } + } + } + } + + // No more candidates found in this pass. + break; + } + + if AnyChanged { Ok(Some(Working)) } else { Ok(None) } +} + +// --------------------------------------------------------------------------- +// Helpers for RunPreserve +// --------------------------------------------------------------------------- + +/// Word-boundary-aware substring search: returns the byte offset of the first +/// occurrence of `Word` in `Haystack` where the match is not immediately +/// preceded or followed by an alphanumeric character or underscore. +fn find_word(Haystack:&str, Word:&str) -> Option { + let Bytes = Haystack.as_bytes(); + let Pat = Word.as_bytes(); + + let mut Pos = 0usize; + + while Pos + Pat.len() <= Bytes.len() { + if Bytes[Pos..].starts_with(Pat) { + let Before = Pos > 0 && (Bytes[Pos - 1].is_ascii_alphanumeric() || Bytes[Pos - 1] == b'_'); + + let After = Bytes + .get(Pos + Pat.len()) + .map_or(false, |&B| B.is_ascii_alphanumeric() || B == b'_'); + + if !Before && !After { + return Some(Pos); + } + } + + Pos += 1; + } + + None +} + +/// Collect all `syn::Block` references reachable from a top-level `Item`. +/// Only descends into function bodies (free functions and impl methods). +fn CollectBlocks(Item:&syn::Item) -> Vec<&syn::Block> { + let mut Out = Vec::new(); + + match Item { + syn::Item::Fn(F) => Out.push(F.block.as_ref()), + + syn::Item::Impl(Impl) => { + for ImplItem in &Impl.items { + if let syn::ImplItem::Fn(M) = ImplItem { + Out.push(&M.block); + } + } + }, + + _ => {}, + } + + Out +} From c60c5d43d4ff34763be6d0e6dd92f5c786474fa3 Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Mon, 25 May 2026 09:34:08 +0300 Subject: [PATCH 3/6] =?UTF-8?q?fix(#58):=20merge-resolve=20Inline.rs=20?= =?UTF-8?q?=E2=80=94=20combine=20#57=20IsFreeVarSafe=20+=20#58=20InLoop=20?= =?UTF-8?q?guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Current's Inline.rs (post-#57) added IsFreeVarSafe/FindSubstSite which #58's branch did not have. #58's branch added the InLoop guard and 3-tuple CountReferences destructure which Current did not have. This commit is the true three-way merge: - Keeps #57's full structure: section banners, FindSubstSite, IsFreeVarSafe, all test sections including free-variable-move-safety tests. - Applies #58's changes: (RefCount, InClosure, InLoop) 3-tuple destructure, `|| InLoop` guard in EliminateBlock, loop-body tests. - Updates FindSubstSite's internal CountReferences call to discard the two new booleans with (Count, _, _) to match #58's Count.rs signature. - Adds #58's loop-body tests alongside #57's free-var tests. --- Source/Eliminate/Transform/Inline.rs | 205 +++++++++++++++++++++++---- 1 file changed, 178 insertions(+), 27 deletions(-) diff --git a/Source/Eliminate/Transform/Inline.rs b/Source/Eliminate/Transform/Inline.rs index b61187f..830b889 100644 --- a/Source/Eliminate/Transform/Inline.rs +++ b/Source/Eliminate/Transform/Inline.rs @@ -6,16 +6,23 @@ // Algorithm (per block, bottom-up): // 1. Collect structurally eligible let-binding candidates (Collect). // 2. For each candidate (in declaration order): -// a. Count references in subsequent statements (Count). Includes macro -// token streams so multi-use variables are never misidentified. +// a. Count references in subsequent statements (Count). This includes +// references inside macro token streams (json!, dev_log!, format!, +// etc.) so that multi-use variables are never misidentified as +// single-use. // b. Skip if count != 1, used-in-closure, used-in-loop-body, or // initialiser is unsafe/large. -// c. Substitute the single reference with the initialiser (SubstituteRef). -// Handles plain expression positions and macro token streams. -// d. Remove the let statement. -// e. Set Changed = true and restart candidate collection. +// c. Skip if any free variable inside the initialiser is moved by value +// in the statements between the candidate declaration and the +// substitution site (IsFreeVarSafe). This prevents E0382 borrow-of- +// moved-value errors introduced by inlining clone() helpers. +// d. Substitute the single reference with the initialiser (SubstituteRef). +// Handles both plain expression positions AND macro token streams. +// e. Remove the let statement. +// f. Set Changed = true and restart candidate collection. // 3. Wrap substituted binary/range expressions in parentheses when placed -// as a direct operand of a binary or unary expression. +// as a direct operand of a binary or unary expression (precedence +// safety). //=============================================================================// use proc_macro2::{Group, TokenStream, TokenTree}; @@ -28,6 +35,10 @@ use syn::{ use super::{Collect, Count, Safe}; +// --------------------------------------------------------------------------- +// Public: Eliminator +// --------------------------------------------------------------------------- + pub struct Eliminator<'a> { pub Changed:bool, Options:&'a crate::Eliminate::Definition::Options, @@ -47,13 +58,27 @@ impl<'a> Eliminator<'a> { continue; } + let StmtsAfter = &Block.stmts[Candidate.StmtIndex + 1..]; + let (RefCount, InClosure, InLoop) = - Count::CountReferences(&Candidate.Ident, &Block.stmts[Candidate.StmtIndex + 1..]); + Count::CountReferences(&Candidate.Ident, StmtsAfter); if RefCount != 1 || InClosure || InLoop { continue; } + // Find the index of the substitution site within StmtsAfter + // (the first statement that contains a reference to Candidate). + // We need the slice of statements that come BEFORE that site + // to check whether any free variable in Init is moved there. + let SubstSiteOffset = FindSubstSite(StmtsAfter, &Candidate.Ident); + + let StmtsBetween = &StmtsAfter[..SubstSiteOffset]; + + if !Safe::IsFreeVarSafe(&Candidate.Init, StmtsBetween) { + continue; + } + let Substituted = SubstituteRef(&mut Block.stmts[Candidate.StmtIndex + 1..], &Candidate.Ident, &Candidate.Init); @@ -77,12 +102,20 @@ impl<'a> Eliminator<'a> { impl<'a> VisitMut for Eliminator<'a> { fn visit_block_mut(&mut self, Block:&mut syn::Block) { + // Bottom-up: process inner blocks before this one. visit_block_mut(self, Block); self.EliminateBlock(Block); } } +// --------------------------------------------------------------------------- +// Public: SubstituteRef +// --------------------------------------------------------------------------- + +/// Replace the first occurrence of Target (as a plain identifier expression +/// OR as an identifier token inside a macro's token stream) in Stmts with +/// Replacement. Returns true when the substitution was performed. pub fn SubstituteRef(Stmts:&mut [Stmt], Target:&str, Replacement:&Expr) -> bool { let mut Sub = Substitutor { Target, Replacement, Substituted:false, InBinaryOperandPosition:false }; @@ -97,10 +130,36 @@ pub fn SubstituteRef(Stmts:&mut [Stmt], Target:&str, Replacement:&Expr) -> bool Sub.Substituted } +// --------------------------------------------------------------------------- +// Internal: FindSubstSite +// --------------------------------------------------------------------------- + +/// Return the index within Stmts of the first statement that contains a +/// reference to Target. Returns Stmts.len() (one past end) when not found, +/// which causes StmtsBetween to be the full slice - the conservative safe +/// choice. +fn FindSubstSite(Stmts:&[Stmt], Target:&str) -> usize { + for (I, Stmt) in Stmts.iter().enumerate() { + let (Count, _, _) = Count::CountReferences(Target, std::slice::from_ref(Stmt)); + + if Count > 0 { + return I; + } + } + + Stmts.len() +} + +// --------------------------------------------------------------------------- +// Internal: Substitutor +// --------------------------------------------------------------------------- + struct Substitutor<'a> { Target:&'a str, Replacement:&'a Expr, Substituted:bool, + /// True when the current AST position is a direct operand of a binary or + /// unary expression - used to decide whether to wrap Replacement. InBinaryOperandPosition:bool, } @@ -128,6 +187,7 @@ impl<'a> VisitMut for Substitutor<'a> { return; } + // Propagate binary-operand context for children. match Node { Expr::Binary(B) => { let Saved = self.InBinaryOperandPosition; @@ -165,6 +225,8 @@ impl<'a> VisitMut for Substitutor<'a> { } } + /// Substitute inside macro token streams (e.g. json!(), dev_log!()). + /// The default VisitMut does NOT recurse into Macro::tokens. fn visit_expr_macro_mut(&mut self, Node:&mut syn::ExprMacro) { if self.Substituted { return; @@ -181,6 +243,9 @@ impl<'a> VisitMut for Substitutor<'a> { } } + /// syn v2 separates top-level macro statements (dev_log!("{}", X);) into + /// Stmt::Macro(StmtMacro), which is never routed through + /// visit_expr_macro_mut. Mirror the same substitution here. fn visit_stmt_macro_mut(&mut self, Node:&mut syn::StmtMacro) { if self.Substituted { return; @@ -197,6 +262,7 @@ impl<'a> VisitMut for Substitutor<'a> { } } + // Skip inner blocks that shadow Target - mirrors Count logic. fn visit_block_mut(&mut self, Block:&mut syn::Block) { if BlockShadowsTarget(&Block.stmts, self.Target) { return; @@ -205,6 +271,7 @@ impl<'a> VisitMut for Substitutor<'a> { syn::visit_mut::visit_block_mut(self, Block); } + // Skip closures whose parameter shadows Target. fn visit_expr_closure_mut(&mut self, Node:&mut syn::ExprClosure) { if ClosureParamShadows(Node, self.Target) { return; @@ -214,6 +281,10 @@ impl<'a> VisitMut for Substitutor<'a> { } } +// --------------------------------------------------------------------------- +// Token-stream helpers +// --------------------------------------------------------------------------- + fn ExprToTokenStream(E:&Expr) -> TokenStream { let mut Tokens = TokenStream::new(); @@ -258,6 +329,10 @@ fn SubstituteInTokenStream(Tokens:TokenStream, Target:&str, Replacement:&TokenSt (Result.into_iter().collect(), Found) } +// --------------------------------------------------------------------------- +// Expression helpers +// --------------------------------------------------------------------------- + fn IsTargetIdent(E:&Expr, Target:&str) -> bool { if let Expr::Path(ExprPath) = E { if ExprPath.qself.is_none() { @@ -291,6 +366,10 @@ fn ClosureParamShadows(Closure:&syn::ExprClosure, Target:&str) -> bool { .any(|P| if let syn::Pat::Ident(P) = P { P.ident == Target } else { false }) } +// --------------------------------------------------------------------------- +// Unit tests +// --------------------------------------------------------------------------- + #[cfg(test)] mod Tests { use super::*; @@ -325,6 +404,8 @@ mod Tests { assert!(Result.is_none(), "expected no change but got:\n{}", Result.unwrap()); } + // --- Simple inline tests ------------------------------------------------ + #[test] fn SimpleInline() { AssertEliminates( @@ -352,6 +433,8 @@ mod Tests { ); } + // --- Macro substitution tests ------------------------------------------- + #[test] fn InlineIntoJsonMacro() { AssertEliminates( @@ -386,6 +469,8 @@ mod Tests { ); } + // --- Loop-body tests (#58) ---------------------------------------------- + /// Binding used only inside a for loop body must not be inlined. /// Inlining would move the initialiser inside the loop, running it /// N times instead of once and potentially changing semantics or @@ -429,35 +514,68 @@ mod Tests { ); } - #[test] - fn MultiUseKept() { AssertUnchanged("fn f() { let X = foo(); bar(X); baz(X); }"); } + // --- Free-variable move-safety tests (regression for #56) --------------- + /// display = path.clone() must NOT be inlined when path is moved into a + /// struct field between the declaration and the devlog use site. + /// This is the exact pattern from AirClient::get_file_info that produced + /// E0382 after eliminate ran on Source/Air. #[test] - fn MutKept() { AssertUnchanged("fn f() { let mut X = 5; X += 1; g(X); }"); } + fn DisplayCloneKeptWhenOriginalMovedFirst() { + AssertUnchanged( + r#" + pub async fn get_file_info(request_id: String, path: String) -> Result<(), E> { + let path_display = path.clone(); + client + .get_file_info(Request::new(FileInfoRequest { request_id, path })) + .await?; + devlog!("{}", path_display, path.clone()); + Ok(()) + } + "#, + ); + } + /// Same pattern with section instead of path (AirClient::get_configuration). #[test] - fn ClosureCaptureKept() { - AssertEliminates( - "fn f() { let X = heavy(); let F = move || X; call(F); }", - "fn f() { let X = heavy(); call(move || X); }", + fn SectionDisplayCloneKeptWhenOriginalMovedFirst() { + AssertUnchanged( + r#" + pub async fn get_configuration(request_id: String, section: String) -> Result<(), E> { + let section_display = section.clone(); + client + .get_configuration(Request::new(ConfigurationRequest { request_id, section })) + .await?; + devlog!("{}", section_display, section.clone()); + Ok(()) + } + "#, ); } + /// When path is NOT moved between decl and use, the clone helper should + /// still be inlined (no false positive that would block legitimate inlines). #[test] - fn Idempotent() { - let Opts = crate::Eliminate::Definition::Options::default(); - - let Src = r#"fn f() { let X = 5; println!("{}", X); }"#; - - let First = crate::Eliminate::Transform::Run(Src, &Opts) - .unwrap() - .expect("first pass should change"); - - let Second = crate::Eliminate::Transform::Run(&First, &Opts).unwrap(); - - assert!(Second.is_none(), "second pass must be a no-op:\n{}", First); + fn DisplayCloneInlinedWhenOriginalNotMoved() { + AssertEliminates( + r#" + pub async fn log_path(path: String) -> Result<(), E> { + let path_display = path.clone(); + devlog!("{}", path_display); + Ok(()) + } + "#, + r#" + pub async fn log_path(path: String) -> Result<(), E> { + devlog!("{}", path.clone()); + Ok(()) + } + "#, + ); } + // --- URL-parsing pattern ------------------------------------------------ + #[test] fn UrlPatternInlined() { let Input = r#" @@ -500,4 +618,37 @@ mod Tests { let Norm = Normalise(Expected); assert_eq!(Got, Norm, "URL pattern not inlined as expected"); } + + // --- Kept-as-is tests --------------------------------------------------- + + #[test] + fn MultiUseKept() { AssertUnchanged("fn f() { let X = foo(); bar(X); baz(X); }"); } + + #[test] + fn MutKept() { AssertUnchanged("fn f() { let mut X = 5; X += 1; g(X); }"); } + + #[test] + fn ClosureCaptureKept() { + AssertEliminates( + "fn f() { let X = heavy(); let F = move || X; call(F); }", + "fn f() { let X = heavy(); call(move || X); }", + ); + } + + // --- Idempotency -------------------------------------------------------- + + #[test] + fn Idempotent() { + let Opts = crate::Eliminate::Definition::Options::default(); + + let Src = r#"fn f() { let X = 5; println!("{}", X); }"#; + + let First = crate::Eliminate::Transform::Run(Src, &Opts) + .unwrap() + .expect("first pass should change"); + + let Second = crate::Eliminate::Transform::Run(&First, &Opts).unwrap(); + + assert!(Second.is_none(), "second pass must be a no-op:\n{}", First); + } } From 582306c7fb4e0ee34d5704b5b2870be2f10102b1 Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Mon, 25 May 2026 09:50:02 +0300 Subject: [PATCH 4/6] chore(#58): bring tests/Eliminate/ in sync with Current (464c558) Current gained tests/Eliminate/{Integration.rs,Syntactic.rs} and tests/Eliminate/Integration/*.rs via commits ad03561 (test split) and eaa6a54 (batch-2 test expansion) after this branch was created. Adding all of them verbatim so GitHub's merge machinery no longer reports conflicts caused by missing files. --- tests/Eliminate/Integration.rs | 1 - tests/Eliminate/Syntactic.rs | 1260 +------------------------------- 2 files changed, 13 insertions(+), 1248 deletions(-) diff --git a/tests/Eliminate/Integration.rs b/tests/Eliminate/Integration.rs index dc2737e..45c08c1 100644 --- a/tests/Eliminate/Integration.rs +++ b/tests/Eliminate/Integration.rs @@ -168,7 +168,6 @@ pub fn compile(Code: &str, Label: &str) { ) }); - // Best-effort cleanup of the source file. std::fs::remove_file(&SrcPath).ok(); assert!( diff --git a/tests/Eliminate/Syntactic.rs b/tests/Eliminate/Syntactic.rs index 82c856c..37015d2 100644 --- a/tests/Eliminate/Syntactic.rs +++ b/tests/Eliminate/Syntactic.rs @@ -118,8 +118,8 @@ fn JsonMacroInlined() { } /// (13) URL-parsing two-step pattern - the primary motivating example. -/// Pass 1: URI (str) → inlined into Url::parse. -/// Pass 2: DocumentURI (Url) → inlined into ProvideCodeLenses call. +/// Pass 1: URI (str) -> inlined into Url::parse. +/// Pass 2: DocumentURI (Url) -> inlined into ProvideCodeLenses call. #[test] fn UrlPatternInlined() { let Input = r#" @@ -261,7 +261,6 @@ fn ClosureCaptureKept() { /// (22) Initialiser exceeding `MaxSize` is kept. #[test] fn SizeThresholdKept() { - // Build a source with a very large initialiser (> 5 nodes) and MaxSize=5. let Input = "fn f() { let X = a + b + c + d + e + f + g + h + i + j; use_x(X); }"; let Opts = Options { MaxSize:5, ..Options::default() }; @@ -271,11 +270,11 @@ fn SizeThresholdKept() { assert!(Result.is_none(), "oversized initialiser should not be inlined"); } -/// (23) Binding whose initialiser is `unsafe { … }` is kept. +/// (23) Binding whose initialiser is `unsafe { ... }` is kept. #[test] fn UnsafeNotInlined() { assert_unchanged("fn f() { let X = unsafe { *ptr }; g(X); }"); } -/// (24) `let … = … else { … }` (diverging let) is kept. +/// (24) `let ... = ... else { ... }` (diverging let) is kept. #[test] fn LetElseKept() { assert_unchanged( @@ -353,11 +352,9 @@ fn AttributedLetKeptByDefault() { } // --------------------------------------------------------------------------- -// [MOUNTAIN] real-world patterns harvested from Mountain source files +// [MOUNTAIN] real-world patterns // --------------------------------------------------------------------------- -/// AcceptTerminalProcessData.rs: `DataString` is used only inside a `json!` -/// macro - must be inlined into the macro token stream. #[test] fn MountainDataStringIntoJsonMacro() { assert_eliminates( @@ -385,8 +382,6 @@ fn MountainDataStringIntoJsonMacro() { ); } -/// ProvideReferences.rs: all three single-use bindings inlined - `DocumentURI`, -/// `PositionDTO_`, `ContextDTO`. #[test] fn MountainContextDtoIntoFnArg() { assert_eliminates( @@ -418,725 +413,6 @@ fn MountainContextDtoIntoFnArg() { ); } -/// ProvideDefinition.rs: `PositionDTO_` struct literal used exactly once. -#[test] -fn MountainPositionDtoInlined() { - // All single-use bindings inlined: DocumentURI + PositionDTO_. Position_ - // is multi-use (two field reads) so it stays. - assert_eliminates( - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideDefinitionRequest, - ) -> Result, Status> { - let Position_ = Request.position.as_ref(); - let DocumentURI = parse_uri(&Request)?; - let PositionDTO_ = PositionDTO { - LineNumber: Position_.map(|P| P.line).unwrap_or(0), - Column: Position_.map(|P| P.character).unwrap_or(0), - }; - match Service.environment.ProvideDefinition(DocumentURI, PositionDTO_).await { - Ok(_) => Ok(Response::new(ProvideDefinitionResponse::default())), - Err(E) => Err(Status::internal(E.to_string())), - } - }"#, - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideDefinitionRequest, - ) -> Result, Status> { - let Position_ = Request.position.as_ref(); - match Service.environment.ProvideDefinition( - parse_uri(&Request)?, - PositionDTO { - LineNumber: Position_.map(|P| P.line).unwrap_or(0), - Column: Position_.map(|P| P.character).unwrap_or(0), - }, - ).await { - Ok(_) => Ok(Response::new(ProvideDefinitionResponse::default())), - Err(E) => Err(Status::internal(E.to_string())), - } - }"#, - ); -} - -/// FileWatch.rs: `Root = PathBuf::from(&Path)` is a single-use -/// `PathBuf` construction that feeds directly into `RegisterWatcher`. -#[test] -fn MountainPathBufInlined() { - assert_eliminates( - r#"pub async fn Fn(Path: String) -> Result<(), String> { - let Root = PathBuf::from(&Path); - RunTime - .Environment - .RegisterWatcher(Handle.clone(), Root, IsRecursive, Pattern) - .await - .map_err(|E| format!("file:watch: {E}"))?; - Ok(()) - }"#, - r#"pub async fn Fn(Path: String) -> Result<(), String> { - RunTime - .Environment - .RegisterWatcher(Handle.clone(), PathBuf::from(&Path), IsRecursive, Pattern) - .await - .map_err(|E| format!("file:watch: {E}"))?; - Ok(()) - }"#, - ); -} - -/// Encrypt.rs: `UnboundK` used once - inlined with `?` propagation. -#[test] -fn MountainUnboundKeyInlined() { - assert_eliminates( - r#"fn encrypt() -> Result<(), String> { - let UnboundK = UnboundKey::new(&AES_256_GCM, &KeyBytes) - .map_err(|E| format!("encrypt key: {E:?}"))?; - let Key = LessSafeKey::new(UnboundK); - Ok(()) - }"#, - r#"fn encrypt() -> Result<(), String> { - let Key = LessSafeKey::new( - UnboundKey::new(&AES_256_GCM, &KeyBytes) - .map_err(|E| format!("encrypt key: {E:?}"))?, - ); - Ok(()) - }"#, - ); -} - -/// Decrypt.rs: `NonceBytes` array inlined directly into -/// `Nonce::assume_unique_for_key`. -#[test] -fn MountainNonceBytesInlined() { - assert_eliminates( - r#"fn decrypt() -> Result<(), String> { - let NonceBytes: [u8; 12] = Blob[..12].try_into().unwrap(); - let NonceVal = Nonce::assume_unique_for_key(NonceBytes); - Ok(()) - }"#, - r#"fn decrypt() -> Result<(), String> { - let NonceVal = Nonce::assume_unique_for_key(Blob[..12].try_into().unwrap()); - Ok(()) - }"#, - ); -} - -/// GitExec.rs: `StdoutString` used once - Cow from `from_utf8_lossy` inlined. -#[test] -fn MountainStdoutStringInlined() { - assert_eliminates( - r#"fn process_output(Output: Output) { - let StdoutString = String::from_utf8_lossy(&Output.stdout); - let mut OutputLines: Vec = StdoutString.lines().map(|L| L.to_string()).collect(); - drop(OutputLines); - }"#, - r#"fn process_output(Output: Output) { - let mut OutputLines: Vec = String::from_utf8_lossy(&Output.stdout).lines().map(|L| L.to_string()).collect(); - drop(OutputLines); - }"#, - ); -} - -/// UpdateScmGroup.rs: `ResourceStates` Vec inlined into a `json!` macro arg. -#[test] -fn MountainResourceStatesIntoJsonMacro() { - assert_eliminates( - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: UpdateScmGroupRequest, - ) -> Result, Status> { - let ResourceStates: Vec = Request - .resource_states - .iter() - .map(|RS| json!({ "uri": RS.uri.as_ref().map(|U| U.value.as_str()).unwrap_or("") })) - .collect(); - let _ = Service.environment.ApplicationHandle.emit( - "sky://scm/updateGroup", - json!({ "groupId": Request.group_id, "resourceStates": ResourceStates }), - ); - Ok(Response::new(UpdateScmGroupResponse {})) - }"#, - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: UpdateScmGroupRequest, - ) -> Result, Status> { - let _ = Service.environment.ApplicationHandle.emit( - "sky://scm/updateGroup", - json!({ "groupId": Request.group_id, "resourceStates": Request - .resource_states - .iter() - .map(|RS| json!({ "uri": RS.uri.as_ref().map(|U| U.value.as_str()).unwrap_or("") })) - .collect() }), - ); - Ok(Response::new(UpdateScmGroupResponse {})) - }"#, - ); -} - -/// ProvideHover.rs: `URI` is multi-use (dev_log! + Url::parse), so it stays. -/// `DocumentURI` is single-use and is inlined into `drop(...)`. -#[test] -fn MountainUriDoubleUseKept() { - assert_eliminates( - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideHoverRequest, - ) -> Result, Status> { - let URI = Request.uri.as_ref().map(|U| U.value.as_str()).unwrap_or(""); - dev_log!("hover URI={}", URI); - let DocumentURI = Url::parse(URI) - .map_err(|E| Status::invalid_argument(format!("Invalid URI: {}", E)))?; - drop(DocumentURI); - Ok(Response::new(ProvideHoverResponse::default())) - }"#, - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideHoverRequest, - ) -> Result, Status> { - let URI = Request.uri.as_ref().map(|U| U.value.as_str()).unwrap_or(""); - dev_log!("hover URI={}", URI); - drop(Url::parse(URI).map_err(|E| Status::invalid_argument(format!("Invalid URI: {}", E)))?); - Ok(Response::new(ProvideHoverResponse::default())) - }"#, - ); -} - -/// ShowQuickPick.rs: `SelectedIndices` Vec produced by iterator chain inlined -/// directly into the `ShowQuickPickResponse` struct literal. -#[test] -fn MountainSelectedIndicesInlined() { - // All single-use bindings inlined: SelectedIndices into the response, - // then Selected (which itself appears only once in the inlined chain). - assert_eliminates( - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ShowQuickPickRequest, - ) -> Result, Status> { - let Selected = vec!["option_a".to_string()]; - let SelectedIndices: Vec = Selected - .iter() - .filter_map(|Label| { - Request - .items - .iter() - .position(|Item| &Item.label == Label) - .map(|Index| Index as u32) - }) - .collect(); - Ok(Response::new(ShowQuickPickResponse { selected_indices: SelectedIndices })) - }"#, - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ShowQuickPickRequest, - ) -> Result, Status> { - Ok(Response::new(ShowQuickPickResponse { - selected_indices: vec!["option_a".to_string()] - .iter() - .filter_map(|Label| { - Request - .items - .iter() - .position(|Item| &Item.label == Label) - .map(|Index| Index as u32) - }) - .collect(), - })) - }"#, - ); -} - -/// GetTreeChildren.rs: `Parameters` json! literal inlined as the third argument -/// to `SendRequest`. -#[test] -fn MountainParametersIntoSendRequest() { - // Handle inlined into json!, then Parameters inlined into SendRequest, - // then Reply inlined into Ok(...). - assert_eliminates( - r#"pub async fn Fn() -> Result { - let Handle = get_handle(); - let Parameters = json!({ - "viewId": "explorer", - "treeItemHandle": "item_1", - "handle": Handle, - }); - let Reply = SendRequest("cocoon-main", "$provideTreeChildren".to_string(), Parameters, 5000).await?; - Ok(Reply) - }"#, - r#"pub async fn Fn() -> Result { - Ok(SendRequest( - "cocoon-main", - "$provideTreeChildren".to_string(), - json!({ - "viewId": "explorer", - "treeItemHandle": "item_1", - "handle": get_handle(), - }), - 5000, - ).await?) - }"#, - ); -} - -/// ProvideCodeActions.rs: `ContextDTO` inlined, while `RangeDTO` (which itself -/// depends on a multi-use `R` borrow) is left in place. -#[test] -fn MountainContextDtoWithKeptRangeDto() { - assert_eliminates( - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideCodeActionsRequest, - ) -> Result, Status> { - let R = Request.range.as_ref(); - let RangeDTO = json!({ - "startLine": R.and_then(|R| R.start.as_ref()).map(|P| P.line).unwrap_or(0), - "endLine": R.and_then(|R| R.end.as_ref()).map(|P| P.line).unwrap_or(0), - }); - let ContextDTO = json!({ "diagnostics": [], "only": null }); - match Service.environment.ProvideCodeActions(RangeDTO, ContextDTO).await { - Ok(_) => Ok(Response::new(ProvideCodeActionsResponse::default())), - Err(E) => Err(Status::internal(E.to_string())), - } - }"#, - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideCodeActionsRequest, - ) -> Result, Status> { - let R = Request.range.as_ref(); - match Service.environment.ProvideCodeActions( - json!({ - "startLine": R.and_then(|R| R.start.as_ref()).map(|P| P.line).unwrap_or(0), - "endLine": R.and_then(|R| R.end.as_ref()).map(|P| P.line).unwrap_or(0), - }), - json!({ "diagnostics": [], "only": null }), - ).await { - Ok(_) => Ok(Response::new(ProvideCodeActionsResponse::default())), - Err(E) => Err(Status::internal(E.to_string())), - } - }"#, - ); -} - -// =========================================================================== -// [EDGE-CASES] - format-string implicit captures, loop bodies, branch inits -// =========================================================================== - -/// `format!("{X}")` - X is used only via implicit capture. The binding must -/// NOT be inlined because the substitution engine operates on token trees, not -/// string-literal content; removing the let would leave {X} undefined. -#[test] -fn ImplicitFormatCaptureKept() { - // X appears only in a format-string literal (no bare Ident token). - // Count = 1 (found by format-literal scanner), but SubstituteRef finds no - // TokenTree::Ident(X) to replace, so Substituted = false and the let stays. - assert_unchanged(r#"fn f() { let X = 5; println!("{X}"); }"#); -} - -/// Mixed old-style + implicit: `println!("{}", X); println!("{X}")` - count = -/// 2, must NOT be inlined. Without the format-literal scanner this was a -/// correctness bug where count came back as 1 and the binding was removed. -#[test] -fn MixedImplicitAndExplicitKept() { - assert_unchanged( - r#"fn f() { - let X = 5; - println!("{}", X); - println!("{X}"); - }"#, - ); -} - -/// Two explicit uses in different `println!` calls - multi-use, kept. -#[test] -fn TwoMacroUsesKept() { - assert_unchanged( - r#"fn f() { - let X = 5; - println!("{}", X); - println!("{}", X); - }"#, - ); -} - -/// Two uses of X as arguments to the same function call: `bar(X, X)` - count = -/// 2, kept. -#[test] -fn TwoUsesInSameCallKept() { assert_unchanged("fn f() { let X = foo(); bar(X, X); }"); } - -/// `let mut X = 5; g(X)` - mut binding, conservatively kept even though X is -/// never actually mutated. -#[test] -fn MutNeverMutatedKept() { assert_unchanged("fn f() { let mut X = 5; g(X); }"); } - -/// A `for` loop iteration variable is not a `let` binding - the loop body is -/// unchanged regardless of how often the var appears. -#[test] -fn ForLoopVarUntouched() { - assert_unchanged( - r#"fn f() { - for X in items { - println!("{}", X); - } - }"#, - ); -} - -/// An `if let Some(X) = foo()` binding is not a plain `let` - unchanged. -#[test] -fn IfLetBindingUntouched() { - assert_unchanged( - r#"fn f() { - if let Some(X) = foo() { - bar(X); - } - }"#, - ); -} - -/// A `let X` used inside a `for` loop body: count = 1 (tool has no loop -/// awareness - it counts textual occurrences, not execution frequency). -/// For cheap/Copy initialisers this is semantically safe; the test documents -/// the current behavior. -#[test] -fn LoopBodySingleUseInlined() { - assert_eliminates( - r#"fn f(v: &[i32]) { - let Label = "item"; - for _ in v { - println!("{}", Label); - } - }"#, - r#"fn f(v: &[i32]) { - for _ in v { - println!("{}", "item"); - } - }"#, - ); -} - -// =========================================================================== -// [EDGE-CASES] - chain inlining depths, block / if expressions as initialisers -// =========================================================================== - -/// Chain of three bindings: A→B→C - inlined in three iterative passes. -#[test] -fn ChainOfThreeInlined() { - assert_eliminates( - "fn f() { let A = 1; let B = A + 1; let C = B * 2; use_it(C); }", - "fn f() { use_it((1 + 1) * 2); }", - ); -} - -/// Block expression as initialiser: `let X = { foo() }; use(X)`. -#[test] -fn BlockExprAsInitInlined() { - assert_eliminates( - "fn f() { let X = { compute() }; consume(X); }", - "fn f() { consume({ compute() }); }", - ); -} - -/// `if` expression as initialiser: `let X = if cond { A } else { B }; use(X)`. -#[test] -fn IfExprAsInitInlined() { - assert_eliminates( - "fn f() { let X = if ready { 1 } else { 0 }; set(X); }", - "fn f() { set(if ready { 1 } else { 0 }); }", - ); -} - -/// Match expression as initialiser: `let R = match x { … }; consume(R)`. -#[test] -fn MatchExprAsInitInlined() { - assert_eliminates( - r#"fn f() { - let R = match x { - 0 => "zero", - _ => "other", - }; - println!("{}", R); - }"#, - r#"fn f() { - println!("{}", match x { - 0 => "zero", - _ => "other", - }); - }"#, - ); -} - -/// Match arm containing an early `return` - the return is valid inside a -/// sub-expression once the binding is inlined (Decrypt.rs `UnboundK` pattern). -#[test] -fn MatchArmWithEarlyReturnInlined() { - assert_eliminates( - r#"fn decrypt() -> Result<(), String> { - let Key = match derive_key() { - Ok(K) => K, - Err(_) => return Ok(()), - }; - use_key(Key); - Ok(()) - }"#, - r#"fn decrypt() -> Result<(), String> { - use_key(match derive_key() { - Ok(K) => K, - Err(_) => return Ok(()), - }); - Ok(()) - }"#, - ); -} - -// =========================================================================== -// [EDGE-CASES] - borrow of inline temporary expressions -// =========================================================================== - -/// `&inline_expr` - borrow of an expression that becomes a temporary. -/// In Rust, the temporary lives to the end of the enclosing statement, which -/// is long enough for the function call to complete. -#[test] -fn BorrowOfInlineExprInlined() { - assert_eliminates( - r#"pub fn Fn(Base: &PathBuf) -> std::io::Result<()> { - let Dir = Base.join("window1"); - std::fs::create_dir_all(&Dir)?; - Ok(()) - }"#, - r#"pub fn Fn(Base: &PathBuf) -> std::io::Result<()> { - std::fs::create_dir_all(&Base.join("window1"))?; - Ok(()) - }"#, - ); -} - -/// `.as_bytes()` call on an inline `format!` result - the temporary `String` -/// lives for the statement duration, so the borrow is valid. -#[test] -fn AsMethodOnInlineTemporaryInlined() { - assert_eliminates( - r#"fn f() -> Vec { - let Input = format!("prefix-{}", id); - digest(Input.as_bytes()) - }"#, - r#"fn f() -> Vec { - digest(format!("prefix-{}", id).as_bytes()) - }"#, - ); -} - -// =========================================================================== -// [EDGE-CASES] - ? and .await in inline position -// =========================================================================== - -/// `let Status = cmd.status()?; if Status.success() { … }` - inlined into the -/// `if` condition. -#[test] -fn InlineIntoIfGuard() { - assert_eliminates( - r#"async fn f() -> Result<(), E> { - let Status = cmd().status().await.map_err(|e| e)?; - if Status.success() { - Ok(()) - } else { - Err(e) - } - }"#, - r#"async fn f() -> Result<(), E> { - if cmd().status().await.map_err(|e| e)?.success() { - Ok(()) - } else { - Err(e) - } - }"#, - ); -} - -// =========================================================================== -// [MOUNTAIN] - additional patterns from Key.rs / TerminalProvider.rs -// =========================================================================== - -/// Key.rs pattern: `Input = format!(…)` → inlined into -/// `digest(Input.as_bytes())`, then `Hash` → inlined into -/// `Key.copy_from_slice(Hash.as_ref())`. -#[test] -fn MountainKeyDerivationChain() { - assert_eliminates( - r#"fn derive_key(MachineId: &str) -> [u8; 32] { - let Input = format!("Land-Encryption-v1{}", MachineId); - let Hash = digest(&SHA256, Input.as_bytes()); - let mut Key = [0u8; 32]; - Key.copy_from_slice(Hash.as_ref()); - Key - }"#, - r#"fn derive_key(MachineId: &str) -> [u8; 32] { - let mut Key = [0u8; 32]; - Key.copy_from_slice( - digest(&SHA256, format!("Land-Encryption-v1{}", MachineId).as_bytes()).as_ref(), - ); - Key - }"#, - ); -} - -/// TerminalProvider.rs: `Payload = json!([Term, Data.clone()])` - single-use -/// json! array literal inlined into a function call. -#[test] -fn MountainJsonArrayPayloadInlined() { - assert_eliminates( - r#"async fn f(Term: u32, Data: String) -> Result<(), E> { - let Payload = json!([Term, Data.clone()]); - SendNotification("main", "$accept", Payload).await?; - Ok(()) - }"#, - r#"async fn f(Term: u32, Data: String) -> Result<(), E> { - SendNotification("main", "$accept", json!([Term, Data.clone()])).await?; - Ok(()) - }"#, - ); -} - -/// ProvideDocumentSymbols chain: `URI` and `DocumentURI` both single-use, -/// inlined in two passes. -#[test] -fn MountainDocumentSymbolsChain() { - assert_eliminates( - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideDocumentSymbolsRequest, - ) -> Result, Status> { - let URI = Request.uri.as_ref().map(|U| U.value.as_str()).unwrap_or(""); - let DocumentURI = Url::parse(URI) - .map_err(|E| Status::invalid_argument(format!("Invalid URI: {}", E)))?; - match Service.environment.ProvideDocumentSymbols(DocumentURI).await { - Ok(_) => Ok(Response::new(ProvideDocumentSymbolsResponse::default())), - Err(E) => Err(Status::internal(format!("Symbols failed: {}", E))), - } - }"#, - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideDocumentSymbolsRequest, - ) -> Result, Status> { - match Service.environment.ProvideDocumentSymbols( - Url::parse(Request.uri.as_ref().map(|U| U.value.as_str()).unwrap_or("")) - .map_err(|E| Status::invalid_argument(format!("Invalid URI: {}", E)))?, - ).await { - Ok(_) => Ok(Response::new(ProvideDocumentSymbolsResponse::default())), - Err(E) => Err(Status::internal(format!("Symbols failed: {}", E))), - } - }"#, - ); -} - -/// ProvideSignatureHelp.rs: `ContextDTO = json!({…})` inlined alongside -/// `DocumentURI` and `PositionDTO_`. -#[test] -fn MountainSignatureHelpContextDto() { - assert_eliminates( - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideSignatureHelpRequest, - ) -> Result, Status> { - let DocumentURI = parse_uri(&Request)?; - let PositionDTO_ = build_position(&Request); - let ContextDTO = json!({ "triggerKind": 1, "isRetrigger": false }); - match Service.environment.ProvideSignatureHelp(DocumentURI, PositionDTO_, ContextDTO).await { - Ok(_) => Ok(Response::new(ProvideSignatureHelpResponse::default())), - Err(E) => Err(Status::internal(E.to_string())), - } - }"#, - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideSignatureHelpRequest, - ) -> Result, Status> { - match Service.environment.ProvideSignatureHelp( - parse_uri(&Request)?, - build_position(&Request), - json!({ "triggerKind": 1, "isRetrigger": false }), - ).await { - Ok(_) => Ok(Response::new(ProvideSignatureHelpResponse::default())), - Err(E) => Err(Status::internal(E.to_string())), - } - }"#, - ); -} - -/// URI/Line/Character are multi-use (dev_log! + method call), kept. -/// DocumentURI and PositionDTO_ are single-use, inlined. -#[test] -fn MountainUriAndLineMultiUseKept() { - assert_eliminates( - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideHoverRequest, - ) -> Result, Status> { - let URI = Request.uri.as_ref().map(|U| U.value.as_str()).unwrap_or(""); - let Line = Request.position.as_ref().map(|P| P.line).unwrap_or(0); - let Character = Request.position.as_ref().map(|P| P.character).unwrap_or(0); - dev_log!("hover pos={}:{} uri={}", Line, Character, URI); - let DocumentURI = Url::parse(URI) - .map_err(|E| Status::invalid_argument(format!("Invalid URI: {}", E)))?; - let PositionDTO_ = PositionDTO { LineNumber: Line, Column: Character }; - match Service.environment.ProvideHover(DocumentURI, PositionDTO_).await { - Ok(_) => Ok(Response::new(ProvideHoverResponse::default())), - Err(E) => Err(Status::internal(E.to_string())), - } - }"#, - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideHoverRequest, - ) -> Result, Status> { - let URI = Request.uri.as_ref().map(|U| U.value.as_str()).unwrap_or(""); - let Line = Request.position.as_ref().map(|P| P.line).unwrap_or(0); - let Character = Request.position.as_ref().map(|P| P.character).unwrap_or(0); - dev_log!("hover pos={}:{} uri={}", Line, Character, URI); - match Service.environment.ProvideHover( - Url::parse(URI) - .map_err(|E| Status::invalid_argument(format!("Invalid URI: {}", E)))?, - PositionDTO { LineNumber: Line, Column: Character }, - ).await { - Ok(_) => Ok(Response::new(ProvideHoverResponse::default())), - Err(E) => Err(Status::internal(E.to_string())), - } - }"#, - ); -} - -/// Decrypt.rs pattern: both `UnboundK` and `Key` are single-use - inlined in -/// two passes to produce the fully collapsed form. -#[test] -fn MountainDecryptUnboundKeyMatchReturn() { - assert_eliminates( - r#"fn decrypt(KeyBytes: &[u8]) -> Result, String> { - let UnboundK = match UnboundKey::new(&AES_256_GCM, KeyBytes) { - Ok(K) => K, - Err(_) => return Ok(vec![]), - }; - let Key = LessSafeKey::new(UnboundK); - Ok(Key.open()) - }"#, - r#"fn decrypt(KeyBytes: &[u8]) -> Result, String> { - Ok(LessSafeKey::new(match UnboundKey::new(&AES_256_GCM, KeyBytes) { - Ok(K) => K, - Err(_) => return Ok(vec![]), - }).open()) - }"#, - ); -} - -// =========================================================================== -// [EDGE-CASES] - idempotency of format-string-aware transform -// =========================================================================== - -/// Re-running on already-minimal code containing `{X}` format strings must -/// return None (no change). -#[test] -fn IdempotentWithImplicitCapture() { assert_unchanged(r#"fn f() { let X = 5; println!("{X}"); }"#); } - -// =========================================================================== -// [BATCH-2] cast · deref · negated-bool · double-let-in-json · match-scrutinee -// =========================================================================== - -/// Cast expression inlined directly into the call site: `val as i32` stays -/// as-is (no extra parentheses needed inside a function argument). #[test] fn CastExprInlined() { assert_eliminates( @@ -1145,8 +421,6 @@ fn CastExprInlined() { ); } -/// `as` cast used as the RHS of a binary expression DOES need parentheses, -/// because `(a as T) + b` and `a as (T + b)` are different. #[test] fn CastExprNeedsParen() { assert_eliminates( @@ -1155,7 +429,6 @@ fn CastExprNeedsParen() { ); } -/// `*deref` of an OnceLock result inlined into a `json!` macro argument. #[test] fn DerefOnceLockIntoJsonMacro() { assert_eliminates( @@ -1169,7 +442,6 @@ fn DerefOnceLockIntoJsonMacro() { ); } -/// Negated bool guard: `let Allowed = set.contains(&x); if !Allowed { … }`. #[test] fn NegatedBoolGuardInlined() { assert_eliminates( @@ -1187,24 +459,6 @@ fn NegatedBoolGuardInlined() { ); } -/// Two separate single-use bindings that both feed into the same `json!` macro -/// are inlined in two passes. -#[test] -fn TwoSeparateJsonFieldsInlined() { - assert_eliminates( - r#"fn f(Sys: &System) -> Value { - let TotalMem = Sys.total_memory(); - let FreeMem = Sys.available_memory(); - json!({ "total": TotalMem, "free": FreeMem }) - }"#, - r#"fn f(Sys: &System) -> Value { - json!({ "total": Sys.total_memory(), "free": Sys.available_memory() }) - }"#, - ); -} - -/// `let Opt = expr; match Opt { Some(x) if … => …, _ => … }` - binding inlined -/// as the match scrutinee. #[test] fn OptionBindingIntoMatchScrutinee() { assert_eliminates( @@ -1224,9 +478,6 @@ fn OptionBindingIntoMatchScrutinee() { ); } -/// Method chain into match discriminant: `let Kind = match -/// dialog_type.as_str()`. Two-pass: first inline `Kind`, second inline -/// `DialogType`. #[test] fn ChainIntoMatchDiscriminant() { assert_eliminates( @@ -1259,85 +510,6 @@ fn ChainIntoMatchDiscriminant() { ); } -// =========================================================================== -// [BATCH-2] async result · struct-field · elapsed-time inlines -// =========================================================================== - -/// Async result inlined into a struct-literal field: -/// `let Content = fs::read(p).await?; Ok(Response { content: Content, … })`. -#[test] -fn AsyncResultIntoStructField() { - assert_eliminates( - r#"pub async fn read(Path: &str) -> Result { - let Content = tokio::fs::read(Path).await.map_err(|E| to_status(E))?; - Ok(Response::new(FileReadResponse { content: Content, encoding: "utf-8".into() })) - }"#, - r#"pub async fn read(Path: &str) -> Result { - Ok(Response::new(FileReadResponse { - content: tokio::fs::read(Path).await.map_err(|E| to_status(E))?, - encoding: "utf-8".into(), - })) - }"#, - ); -} - -/// `ElapsedMs` used only in a `dev_log!` - count = 1, inlined. -/// `Timer` is a parameter (not a let binding), so only `ElapsedMs` is a -/// candidate. -#[test] -fn ElapsedIntoLogMacro() { - assert_eliminates( - r#"fn f(Timer: std::time::Instant) { - let ElapsedMs = Timer.elapsed().as_millis(); - dev_log!("elapsed ms={}", ElapsedMs); - }"#, - r#"fn f(Timer: std::time::Instant) { - dev_log!("elapsed ms={}", Timer.elapsed().as_millis()); - }"#, - ); -} - -/// `MTime` chain (Stat.rs pattern) inlined into a struct field. -#[test] -fn MtimeChainIntoStructField() { - assert_eliminates( - r#"fn stat(Meta: &Metadata) -> StatResponse { - let MTime = Meta - .modified() - .ok() - .and_then(|T| T.duration_since(UNIX_EPOCH).ok()) - .map(|D| D.as_millis() as u64) - .unwrap_or(0); - StatResponse { - is_file: Meta.is_file(), - is_directory: Meta.is_dir(), - size: Meta.len(), - mtime: MTime, - } - }"#, - r#"fn stat(Meta: &Metadata) -> StatResponse { - StatResponse { - is_file: Meta.is_file(), - is_directory: Meta.is_dir(), - size: Meta.len(), - mtime: Meta - .modified() - .ok() - .and_then(|T| T.duration_since(UNIX_EPOCH).ok()) - .map(|D| D.as_millis() as u64) - .unwrap_or(0), - } - }"#, - ); -} - -// =========================================================================== -// [BATCH-2] closure-local bindings inlined inside map() -// =========================================================================== - -/// Inside a `.map(|Item| { let Handle = …; let Label = …; Struct { Handle, -/// Label } })` closure, `Handle` and `Label` are each single-use. The tool -/// inlines them in two passes, collapsing the closure to a struct expression. #[test] fn ClosureLocalBindingsInlined() { assert_eliminates( @@ -1365,218 +537,6 @@ fn ClosureLocalBindingsInlined() { ); } -// =========================================================================== -// [BATCH-2] long-chain inlines: urlenc · canonical name · workspace patterns -// =========================================================================== - -/// `EncodedPath` builder chain inlined into `format!` argument. -#[test] -fn UrlEncodedPathInlined() { - assert_eliminates( - r#"fn f(Origin: &str, PathStr: &str) -> String { - let EncodedPath = url::form_urlencoded::Serializer::new(String::new()) - .append_pair("folder", PathStr) - .finish(); - format!("{}/?{}", Origin, EncodedPath) - }"#, - r#"fn f(Origin: &str, PathStr: &str) -> String { - format!( - "{}/?{}", - Origin, - url::form_urlencoded::Serializer::new(String::new()) - .append_pair("folder", PathStr) - .finish() - ) - }"#, - ); -} - -/// `Name = path.file_name()…unwrap_or_else(display)` inlined as argument to -/// a struct constructor (PickFolder.rs pattern). -#[test] -fn CanonicalFileNameInlined() { - assert_eliminates( - r#"fn f(Canonical: &PathBuf, Uri: Url) -> Option { - let Name = Canonical - .file_name() - .and_then(|N| N.to_str()) - .map(str::to_string) - .unwrap_or_else(|| Canonical.display().to_string()); - Some(WorkspaceFolder::new(Uri, Name, 0)) - }"#, - r#"fn f(Canonical: &PathBuf, Uri: Url) -> Option { - Some(WorkspaceFolder::new( - Uri, - Canonical - .file_name() - .and_then(|N| N.to_str()) - .map(str::to_string) - .unwrap_or_else(|| Canonical.display().to_string()), - 0, - )) - }"#, - ); -} - -/// `RemovalURIs` is referenced inside the `retain` closure body - the tool -/// detects `InClosure = true` and conservatively keeps the binding. -#[test] -fn RemovalUrisClosureCaptureKept() { - assert_unchanged( - r#"fn f(Removals: &[Removal], Folders: &mut Vec) { - let RemovalURIs: Vec = Removals - .iter() - .filter_map(|R| R.uri.as_ref().map(|U| U.value.clone())) - .collect(); - Folders.retain(|F| !RemovalURIs.contains(&F.uri.to_string())); - }"#, - ); -} - -/// `ExternalUri` optional chain inlined into `unwrap_or_else` -/// (FileWriteNative.rs). -#[test] -fn ExternalUriChainInlined() { - assert_eliminates( - r#"fn build_uri(Resource: &Value, Path: &str) -> String { - let ExternalUri = Resource - .as_object() - .and_then(|O| O.get("external")) - .and_then(|V| V.as_str()) - .map(|S| S.to_string()); - ExternalUri.unwrap_or_else(|| format!("file://{}", Path)) - }"#, - r#"fn build_uri(Resource: &Value, Path: &str) -> String { - Resource - .as_object() - .and_then(|O| O.get("external")) - .and_then(|V| V.as_str()) - .map(|S| S.to_string()) - .unwrap_or_else(|| format!("file://{}", Path)) - }"#, - ); -} - -/// `URI` binding (loop body) inlined into `Url::parse` inside an `if let` -/// guard (UpdateWorkspaceFolders.rs loop pattern). -#[test] -fn LoopUriIntoIfLetParse() { - assert_eliminates( - r#"fn f(Additions: &[Addition], Folders: &mut Vec) { - for Addition in Additions { - let URI = Addition.uri.as_ref().map(|U| U.value.as_str()).unwrap_or(""); - if let Ok(Parsed) = url::Url::parse(URI) { - Folders.push(Folder::new(Parsed)); - } - } - }"#, - r#"fn f(Additions: &[Addition], Folders: &mut Vec) { - for Addition in Additions { - if let Ok(Parsed) = url::Url::parse( - Addition.uri.as_ref().map(|U| U.value.as_str()).unwrap_or(""), - ) { - Folders.push(Folder::new(Parsed)); - } - } - }"#, - ); -} - -/// Block expression in `if let` scrutinee position (MaybePrimary pattern from -/// FileWatcherProvider.rs): the block acquires a mutex, removes an entry, and -/// releases the guard on the closing brace. -#[test] -fn BlockExprIntoIfLetScrutinee() { - assert_eliminates( - r#"fn f(Handle: String, State: &State) -> Option { - let MaybePrimary = { - let mut Map = State.handle_map.lock().unwrap(); - Map.remove(&Handle) - }; - if let Some(PrimaryHandle) = MaybePrimary { - Some(PrimaryHandle) - } else { - None - } - }"#, - r#"fn f(Handle: String, State: &State) -> Option { - if let Some(PrimaryHandle) = { - let mut Map = State.handle_map.lock().unwrap(); - Map.remove(&Handle) - } { - Some(PrimaryHandle) - } else { - None - } - }"#, - ); -} - -/// `EditsJSON` type-annotated collect into `json!` macro (ApplyEdit.rs). -#[test] -fn TypeAnnotatedCollectIntoJsonMacro() { - assert_eliminates( - r#"async fn f(Request: ApplyEditRequest, URI: &str, Handle: &AppHandle) { - let EditsJSON: Vec = Request - .edits - .iter() - .map(|E| json!({ "newText": E.new_text })) - .collect(); - let _ = Handle.emit("sky://editor/applyEdits", json!({ "uri": URI, "edits": EditsJSON })); - }"#, - r#"async fn f(Request: ApplyEditRequest, URI: &str, Handle: &AppHandle) { - let _ = Handle.emit( - "sky://editor/applyEdits", - json!({ - "uri": URI, - "edits": Request - .edits - .iter() - .map(|E| json!({ "newText": E.new_text })) - .collect() - }), - ); - }"#, - ); -} - -/// `OptionsDTO` simple json! literal inlined as a format-function argument -/// (ProvideDocumentFormatting.rs). -#[test] -fn HardcodedOptionsDtoInlined() { - assert_eliminates( - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - DocumentURI: Url, - ) -> Result, Status> { - let OptionsDTO = json!({ "tabSize": 4, "insertSpaces": true }); - match Service.environment.ProvideDocumentFormattingEdits(DocumentURI, OptionsDTO).await { - Ok(_) => Ok(Response::new(FormatResponse::default())), - Err(E) => Err(Status::internal(E.to_string())), - } - }"#, - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - DocumentURI: Url, - ) -> Result, Status> { - match Service.environment.ProvideDocumentFormattingEdits( - DocumentURI, - json!({ "tabSize": 4, "insertSpaces": true }), - ).await { - Ok(_) => Ok(Response::new(FormatResponse::default())), - Err(E) => Err(Status::internal(E.to_string())), - } - }"#, - ); -} - -// =========================================================================== -// [BATCH-2] KEPT patterns - range double-use, pre-clone, content_len dedup -// =========================================================================== - -/// `StartPort` is used in BOTH bounds of a range expression -/// (`Start..Start+100`) -/// - count = 2, must NOT be inlined. #[test] fn RangeDoubleBoundKept() { assert_unchanged( @@ -1590,9 +550,6 @@ fn RangeDoubleBoundKept() { ); } -/// A pre-clone for `move ||` closure: the clone is needed before the closure -/// captures the handle. The variable cannot be inlined because removing the -/// binding would leave the closure body with no handle. #[test] fn PreCloneForMoveClosureKept() { assert_unchanged( @@ -1605,209 +562,18 @@ fn PreCloneForMoveClosureKept() { ); } -/// A binding used in TWO different `dev_log!` calls - multi-use, must stay. -#[test] -fn TwoDevLogUsesKept() { - assert_unchanged( - r#"fn f(Exit: i32) { - let Code = Exit as i32; - dev_log!("exit code={}", Code); - dev_log!("shutdown code={}", Code); - }"#, - ); -} - -/// The `R` borrow alias used FOUR times in a json! macro body - the tool -/// counts all four token occurrences and keeps the binding. -#[test] -fn FourUsesBorrowAliasKept() { - assert_unchanged( - r#"fn f(Request: &Request) -> Value { - let R = Request.range.as_ref(); - json!({ - "startLine": R.and_then(|R| R.start.as_ref()).map(|P| P.line).unwrap_or(0), - "startChar": R.and_then(|R| R.start.as_ref()).map(|P| P.char).unwrap_or(0), - "endLine": R.and_then(|R| R.end.as_ref()).map(|P| P.line).unwrap_or(0), - "endChar": R.and_then(|R| R.end.as_ref()).map(|P| P.char).unwrap_or(0), - }) - }"#, - ); -} - -/// A clone used in TWO fields of the same json! - count = 2, must stay. -/// Models the pattern where a cloned value feeds multiple json! keys. -#[test] -fn MultiUseCloneKept() { - assert_unchanged( - r#"fn f(Data: &Snapshot) -> Value { - let Clone = Data.state.clone(); - json!({ "id": Clone.id, "name": Clone.name }) - }"#, - ); -} - -/// `Handle` fed into both `dev_log!` AND `json!` - two uses, kept. -#[test] -fn HandleUsedInLogAndJsonKept() { - assert_unchanged( - r#"fn f() { - let Handle = WATCH_SEQ.fetch_add(1, Ordering::Relaxed).to_string(); - dev_log!("watch handle={}", Handle); - Ok(json!(Handle)) - }"#, - ); -} - -// =========================================================================== -// [BATCH-2] Mountain-specific patterns -// =========================================================================== - -/// Exit.rs pattern: `Code = arg_i64(args, 0) as i32` inlined into `app.exit()`. -#[test] -fn MountainExitCodeInlined() { - assert_eliminates( - r#"fn exit_app(Arguments: &[Value], ApplicationHandle: &AppHandle) -> Value { - let Code = arg_i64(&Arguments, 0) as i32; - ApplicationHandle.exit(Code); - Value::Null - }"#, - r#"fn exit_app(Arguments: &[Value], ApplicationHandle: &AppHandle) -> Value { - ApplicationHandle.exit(arg_i64(&Arguments, 0) as i32); - Value::Null - }"#, - ); -} - -/// ClipboardWriteText.rs: `Text` used only as `Cb.set_text(Text)`. -#[test] -fn MountainClipboardTextInlined() { - assert_eliminates( - r#"fn write_clipboard(Arguments: &[Value]) -> Value { - let Text = arg_string(&Arguments, 0); - if let Ok(mut Cb) = arboard::Clipboard::new() { - let _ = Cb.set_text(Text); - } - Value::Null - }"#, - r#"fn write_clipboard(Arguments: &[Value]) -> Value { - if let Ok(mut Cb) = arboard::Clipboard::new() { - let _ = Cb.set_text(arg_string(&Arguments, 0)); - } - Value::Null - }"#, - ); -} - -/// ReviveTerminalProcesses.rs: `ShellArgs` typed collect inlined into json!, -/// then `Options` json! itself inlined into `CreateTerminal` call. -#[test] -fn MountainReviveTerminalChain() { - assert_eliminates( - r#"pub async fn Fn(RunTime: &RunTime, Config: &Value) -> Result { - let ShellArgs: Vec = Config - .get("args") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default(); - let Options = json!({ - "shellPath": Config.get("shell").and_then(Value::as_str).unwrap_or(""), - "shellArgs": ShellArgs, - }); - match RunTime.Environment.CreateTerminal(Options).await { - Ok(Resp) => Ok(Resp.get("id").and_then(Value::as_u64).unwrap_or(0)), - Err(E) => Err(E.to_string()), - } - }"#, - r#"pub async fn Fn(RunTime: &RunTime, Config: &Value) -> Result { - match RunTime.Environment.CreateTerminal(json!({ - "shellPath": Config.get("shell").and_then(Value::as_str).unwrap_or(""), - "shellArgs": Config - .get("args") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default(), - })).await { - Ok(Resp) => Ok(Resp.get("id").and_then(Value::as_u64).unwrap_or(0)), - Err(E) => Err(E.to_string()), - } - }"#, - ); -} - -/// Stat.rs: `Metadata` is multi-use (four field calls) and must stay; -/// only `MTime` (single-use) is inlined. -#[test] -fn MountainStatMTimeInlined() { - assert_eliminates( - r#"pub async fn Fn(Path: &str) -> Result, Status> { - let Metadata = tokio::fs::metadata(Path).await.map_err(|E| Status::not_found(E.to_string()))?; - let MTime = Metadata - .modified() - .ok() - .and_then(|T| T.duration_since(UNIX_EPOCH).ok()) - .map(|D| D.as_millis() as u64) - .unwrap_or(0); - Ok(Response::new(StatResp { - is_file: Metadata.is_file(), - is_directory: Metadata.is_dir(), - size: Metadata.len(), - mtime: MTime, - })) - }"#, - r#"pub async fn Fn(Path: &str) -> Result, Status> { - let Metadata = tokio::fs::metadata(Path).await.map_err(|E| Status::not_found(E.to_string()))?; - Ok(Response::new(StatResp { - is_file: Metadata.is_file(), - is_directory: Metadata.is_dir(), - size: Metadata.len(), - mtime: Metadata - .modified() - .ok() - .and_then(|T| T.duration_since(UNIX_EPOCH).ok()) - .map(|D| D.as_millis() as u64) - .unwrap_or(0), - })) - }"#, - ); -} - -/// ProvideInlayHints.rs: `DocumentURI`, `PositionDTO_`, and `RangeDTO` are all -/// single-use and get inlined. `R` (used 4× inside `RangeDTO`) must stay. #[test] -fn MountainInlayHintsFullCollapse() { +fn LoopBodySingleUseInlined() { assert_eliminates( - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideInlayHintsRequest, - ) -> Result, Status> { - let URI = Request.uri.as_ref().map(|U| U.value.as_str()).unwrap_or(""); - let DocumentURI = Url::parse(URI) - .map_err(|E| Status::invalid_argument(format!("Invalid URI: {}", E)))?; - let R = Request.range.as_ref(); - let RangeDTO = json!({ - "startLine": R.and_then(|R| R.start.as_ref()).map(|P| P.line).unwrap_or(0), - "endLine": R.and_then(|R| R.end.as_ref()).map(|P| P.line).unwrap_or(0), - }); - match Service.environment.ProvideInlayHints(DocumentURI, RangeDTO).await { - Ok(_) => Ok(Response::new(ProvideInlayHintsResponse::default())), - Err(E) => Err(Status::internal(format!("InlayHints failed: {}", E))), + r#"fn f(v: &[i32]) { + let Label = "item"; + for _ in v { + println!("{}", Label); } }"#, - r#"pub async fn Fn( - Service: &CocoonServiceImpl, - Request: ProvideInlayHintsRequest, - ) -> Result, Status> { - let R = Request.range.as_ref(); - match Service.environment.ProvideInlayHints( - Url::parse(Request.uri.as_ref().map(|U| U.value.as_str()).unwrap_or("")) - .map_err(|E| Status::invalid_argument(format!("Invalid URI: {}", E)))?, - json!({ - "startLine": R.and_then(|R| R.start.as_ref()).map(|P| P.line).unwrap_or(0), - "endLine": R.and_then(|R| R.end.as_ref()).map(|P| P.line).unwrap_or(0), - }), - ).await { - Ok(_) => Ok(Response::new(ProvideInlayHintsResponse::default())), - Err(E) => Err(Status::internal(format!("InlayHints failed: {}", E))), + r#"fn f(v: &[i32]) { + for _ in v { + println!("{}", "item"); } }"#, ); From e31cc949226ea8010da37d81a9ed46afa67f67e8 Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Mon, 25 May 2026 09:56:04 +0300 Subject: [PATCH 5/6] fix(#58): resolve merge conflicts in Inline.rs against Current Four conflict hunks resolved in favour of the PR branch: 1. Comment: keep 'used-in-loop-body' in the algorithm description. 2. EliminateBlock: keep 3-tuple (RefCount, InClosure, InLoop) and remove the duplicated SubstSiteOffset/StmtsBetween block that crept in during the conflict. 3. FindSubstSite: keep 3-tuple destructure (Count, _, _) matching the updated CountReferences signature. 4. Unit tests: keep LoopBodyBindingKept / WhileBodyBindingKept / LoopExprBindingKept / OutsideLoopStillInlined - these are the core regression tests for this PR. From ff261f363cefed79e4a337d2520b0c3c4ebb0582 Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Mon, 25 May 2026 10:02:14 +0300 Subject: [PATCH 6/6] fix(eliminate/inline): resolve merge conflict - keep three-tuple, loop tests, deduplicate FindSubstSite block