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 d477024..0a78be6 100644 --- a/Source/Eliminate/Transform/Inline.rs +++ b/Source/Eliminate/Transform/Inline.rs @@ -10,8 +10,8 @@ // 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. +// b. Skip if count != 1, used-in-closure, used-in-loop-body, or +// initialiser is unsafe/large. // 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- @@ -60,10 +60,22 @@ impl<'a> Eliminator<'a> { let StmtsAfter = &Block.stmts[Candidate.StmtIndex + 1..]; - let (RefCount, InClosure) = + let (RefCount, InClosure, InLoop) = Count::CountReferences(&Candidate.Ident, StmtsAfter); - if RefCount != 1 || InClosure { + 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; } @@ -140,7 +152,7 @@ pub fn SubstituteRef(Stmts:&mut [Stmt], Target:&str, Replacement:&Expr) -> bool /// 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)); + let (Count, _, _) = Count::CountReferences(Target, std::slice::from_ref(Stmt)); if Count > 0 { return I; @@ -469,6 +481,51 @@ 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 + /// 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()); }", + ); + } + // --- Free-variable move-safety tests (regression for #56) --------------- /// display = path.clone() must NOT be inlined when path is moved into a 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 +} 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"); } }"#, );