Skip to main content

Maintain/Eliminate/Transform/
Inline.rs

1//=============================================================================//
2// File Path: Element/Maintain/Source/Eliminate/Transform/Inline.rs
3//=============================================================================//
4// Module: Inline - VisitMut transformer that eliminates single-use bindings
5//
6// Algorithm (per block, bottom-up):
7//   1. Collect structurally eligible let-binding candidates (Collect).
8//   2. For each candidate (in declaration order): a. Count references in
9//      subsequent statements (Count). This includes references inside macro
10//      token streams (json!, dev_log!, format!, etc.) so that multi-use
11//      variables are never misidentified as single-use. b. Skip if count != 1,
12//      used-in-closure, used-in-loop-body, or initialiser is unsafe/large. c.
13//      Skip if any free variable inside the initialiser is moved by value in
14//      the statements between the candidate declaration and the substitution
15//      site (IsFreeVarSafe). This prevents E0382 borrow-of- moved-value errors
16//      introduced by inlining clone() helpers. d. Substitute the single
17//      reference with the initialiser (SubstituteRef). Handles both plain
18//      expression positions AND macro token streams. e. Remove the let
19//      statement. f. Set Changed = true and restart candidate collection.
20//   3. Wrap substituted binary/range expressions in parentheses when placed as
21//      a direct operand of a binary or unary expression (precedence safety).
22//=============================================================================//
23
24use proc_macro2::{Group, TokenStream, TokenTree};
25use quote::ToTokens;
26use syn::{
27	Expr,
28	Stmt,
29	visit_mut::{VisitMut, visit_block_mut, visit_expr_mut},
30};
31
32use super::{Collect, Count, Safe};
33
34// ---------------------------------------------------------------------------
35// Public: Eliminator
36// ---------------------------------------------------------------------------
37
38pub struct Eliminator<'a> {
39	pub Changed:bool,
40
41	Options:&'a crate::Eliminate::Definition::Options,
42}
43
44impl<'a> Eliminator<'a> {
45	pub fn new(Options:&'a crate::Eliminate::Definition::Options) -> Self { Self { Changed:false, Options } }
46
47	fn EliminateBlock(&mut self, Block:&mut syn::Block) {
48		loop {
49			let Candidates = Collect::Collect(Block, self.Options.InlineComments);
50
51			let mut DidChange = false;
52
53			for Candidate in &Candidates {
54				if !Safe::IsSafe(&Candidate.Init, self.Options.MaxSize) {
55					continue;
56				}
57
58				let StmtsAfter = &Block.stmts[Candidate.StmtIndex + 1..];
59
60				let (RefCount, InClosure, InLoop) = Count::CountReferences(&Candidate.Ident, StmtsAfter);
61
62				if RefCount != 1 || InClosure || InLoop {
63					continue;
64				}
65
66				// Find the index of the substitution site within StmtsAfter
67				// (the first statement that contains a reference to Candidate).
68				// We need the slice of statements that come BEFORE that site
69				// to check whether any free variable in Init is moved there.
70				let SubstSiteOffset = FindSubstSite(StmtsAfter, &Candidate.Ident);
71
72				let StmtsBetween = &StmtsAfter[..SubstSiteOffset];
73
74				if !Safe::IsFreeVarSafe(&Candidate.Init, StmtsBetween) {
75					continue;
76				}
77
78				let Substituted =
79					SubstituteRef(&mut Block.stmts[Candidate.StmtIndex + 1..], &Candidate.Ident, &Candidate.Init);
80
81				if Substituted {
82					Block.stmts.remove(Candidate.StmtIndex);
83
84					self.Changed = true;
85
86					DidChange = true;
87
88					break;
89				}
90			}
91
92			if !DidChange {
93				break;
94			}
95		}
96	}
97}
98
99impl<'a> VisitMut for Eliminator<'a> {
100	fn visit_block_mut(&mut self, Block:&mut syn::Block) {
101		// Bottom-up: process inner blocks before this one.
102		visit_block_mut(self, Block);
103
104		self.EliminateBlock(Block);
105	}
106}
107
108// ---------------------------------------------------------------------------
109// Public: SubstituteRef
110// ---------------------------------------------------------------------------
111
112/// Replace the first occurrence of Target (as a plain identifier expression
113/// OR as an identifier token inside a macro's token stream) in Stmts with
114/// Replacement. Returns true when the substitution was performed.
115pub fn SubstituteRef(Stmts:&mut [Stmt], Target:&str, Replacement:&Expr) -> bool {
116	let mut Sub = Substitutor { Target, Replacement, Substituted:false, InBinaryOperandPosition:false };
117
118	for Stmt in Stmts.iter_mut() {
119		if Sub.Substituted {
120			break;
121		}
122
123		Sub.visit_stmt_mut(Stmt);
124	}
125
126	Sub.Substituted
127}
128
129// ---------------------------------------------------------------------------
130// Internal: FindSubstSite
131// ---------------------------------------------------------------------------
132
133/// Return the index within Stmts of the first statement that contains a
134/// reference to Target. Returns Stmts.len() (one past end) when not found,
135/// which causes StmtsBetween to be the full slice - the conservative safe
136/// choice.
137pub fn FindSubstSite(Stmts:&[Stmt], Target:&str) -> usize {
138	for (I, Stmt) in Stmts.iter().enumerate() {
139		let (Count, ..) = Count::CountReferences(Target, std::slice::from_ref(Stmt));
140
141		if Count > 0 {
142			return I;
143		}
144	}
145
146	Stmts.len()
147}
148
149// ---------------------------------------------------------------------------
150// Internal: Substitutor
151// ---------------------------------------------------------------------------
152
153struct Substitutor<'a> {
154	Target:&'a str,
155
156	Replacement:&'a Expr,
157
158	Substituted:bool,
159
160	/// True when the current AST position is a direct operand of a binary or
161	/// unary expression - used to decide whether to wrap Replacement.
162	InBinaryOperandPosition:bool,
163}
164
165impl<'a> VisitMut for Substitutor<'a> {
166	fn visit_expr_mut(&mut self, Node:&mut Expr) {
167		if self.Substituted {
168			return;
169		}
170
171		if IsTargetIdent(Node, self.Target) {
172			let NeedsWrapping = self.InBinaryOperandPosition && NeedsParen(self.Replacement);
173
174			*Node = if NeedsWrapping {
175				Expr::Paren(syn::ExprParen {
176					attrs:vec![],
177					paren_token:Default::default(),
178					expr:Box::new(self.Replacement.clone()),
179				})
180			} else {
181				self.Replacement.clone()
182			};
183
184			self.Substituted = true;
185
186			return;
187		}
188
189		// Propagate binary-operand context for children.
190		match Node {
191			Expr::Binary(B) => {
192				let Saved = self.InBinaryOperandPosition;
193
194				self.InBinaryOperandPosition = true;
195
196				self.visit_expr_mut(&mut B.left);
197
198				if !self.Substituted {
199					self.visit_expr_mut(&mut B.right);
200				}
201
202				self.InBinaryOperandPosition = Saved;
203			},
204
205			Expr::Unary(U) => {
206				let Saved = self.InBinaryOperandPosition;
207
208				self.InBinaryOperandPosition = true;
209
210				self.visit_expr_mut(&mut U.expr);
211
212				self.InBinaryOperandPosition = Saved;
213			},
214
215			_ => {
216				let Saved = self.InBinaryOperandPosition;
217
218				self.InBinaryOperandPosition = false;
219
220				visit_expr_mut(self, Node);
221
222				self.InBinaryOperandPosition = Saved;
223			},
224		}
225	}
226
227	/// Substitute inside macro token streams (e.g. json!(), dev_log!()).
228	/// The default VisitMut does NOT recurse into Macro::tokens.
229	fn visit_expr_macro_mut(&mut self, Node:&mut syn::ExprMacro) {
230		if self.Substituted {
231			return;
232		}
233
234		let ReplacementTokens = ExprToTokenStream(self.Replacement);
235
236		let (NewTokens, Found) = SubstituteInTokenStream(Node.mac.tokens.clone(), self.Target, &ReplacementTokens);
237
238		if Found {
239			Node.mac.tokens = NewTokens;
240
241			self.Substituted = true;
242		}
243	}
244
245	/// syn v2 separates top-level macro statements (dev_log!("{}", X);) into
246	/// Stmt::Macro(StmtMacro), which is never routed through
247	/// visit_expr_macro_mut. Mirror the same substitution here.
248	fn visit_stmt_macro_mut(&mut self, Node:&mut syn::StmtMacro) {
249		if self.Substituted {
250			return;
251		}
252
253		let ReplacementTokens = ExprToTokenStream(self.Replacement);
254
255		let (NewTokens, Found) = SubstituteInTokenStream(Node.mac.tokens.clone(), self.Target, &ReplacementTokens);
256
257		if Found {
258			Node.mac.tokens = NewTokens;
259
260			self.Substituted = true;
261		}
262	}
263
264	// Skip inner blocks that shadow Target - mirrors Count logic.
265	fn visit_block_mut(&mut self, Block:&mut syn::Block) {
266		if BlockShadowsTarget(&Block.stmts, self.Target) {
267			return;
268		}
269
270		syn::visit_mut::visit_block_mut(self, Block);
271	}
272
273	// Skip closures whose parameter shadows Target.
274	fn visit_expr_closure_mut(&mut self, Node:&mut syn::ExprClosure) {
275		if ClosureParamShadows(Node, self.Target) {
276			return;
277		}
278
279		syn::visit_mut::visit_expr_closure_mut(self, Node);
280	}
281}
282
283// ---------------------------------------------------------------------------
284// Token-stream helpers
285// ---------------------------------------------------------------------------
286
287fn ExprToTokenStream(E:&Expr) -> TokenStream {
288	let mut Tokens = TokenStream::new();
289
290	E.to_tokens(&mut Tokens);
291
292	Tokens
293}
294
295fn SubstituteInTokenStream(Tokens:TokenStream, Target:&str, Replacement:&TokenStream) -> (TokenStream, bool) {
296	let mut Result:Vec<TokenTree> = Vec::new();
297
298	let mut Found = false;
299
300	for Tree in Tokens {
301		if Found {
302			Result.push(Tree);
303
304			continue;
305		}
306
307		match Tree {
308			TokenTree::Ident(ref I) if I.to_string() == Target => {
309				Result.extend(Replacement.clone());
310
311				Found = true;
312			},
313
314			TokenTree::Group(G) => {
315				let (NewStream, F) = SubstituteInTokenStream(G.stream(), Target, Replacement);
316
317				if F {
318					Found = true;
319				}
320
321				Result.push(TokenTree::Group(Group::new(G.delimiter(), NewStream)));
322			},
323
324			Other => Result.push(Other),
325		}
326	}
327
328	(Result.into_iter().collect(), Found)
329}
330
331// ---------------------------------------------------------------------------
332// Expression helpers
333// ---------------------------------------------------------------------------
334
335fn IsTargetIdent(E:&Expr, Target:&str) -> bool {
336	if let Expr::Path(ExprPath) = E {
337		if ExprPath.qself.is_none() {
338			if let Some(Ident) = ExprPath.path.get_ident() {
339				return Ident == Target;
340			}
341		}
342	}
343
344	false
345}
346
347fn NeedsParen(E:&Expr) -> bool { matches!(E, Expr::Binary(_) | Expr::Range(_) | Expr::Closure(_) | Expr::Cast(_)) }
348
349fn BlockShadowsTarget(Stmts:&[Stmt], Target:&str) -> bool {
350	Stmts.iter().any(|S| {
351		if let Stmt::Local(L) = S {
352			if let syn::Pat::Ident(P) = &L.pat {
353				return P.ident == Target;
354			}
355		}
356
357		false
358	})
359}
360
361fn ClosureParamShadows(Closure:&syn::ExprClosure, Target:&str) -> bool {
362	Closure
363		.inputs
364		.iter()
365		.any(|P| if let syn::Pat::Ident(P) = P { P.ident == Target } else { false })
366}
367
368// ---------------------------------------------------------------------------
369// Unit tests
370// ---------------------------------------------------------------------------
371
372#[cfg(test)]
373mod Tests {
374
375	use super::*;
376
377	fn Transform(Src:&str) -> String {
378		let Opts = crate::Eliminate::Definition::Options::default();
379
380		crate::Eliminate::Transform::Run(Src, &Opts)
381			.expect("transform failed")
382			.unwrap_or_else(|| {
383				let Ast:syn::File = syn::parse_str(Src).unwrap();
384
385				prettyplease::unparse(&Ast)
386			})
387	}
388
389	fn Normalise(Src:&str) -> String {
390		let Ast:syn::File = syn::parse_str(Src).unwrap();
391
392		prettyplease::unparse(&Ast)
393	}
394
395	fn AssertEliminates(Input:&str, Expected:&str) {
396		assert_eq!(Transform(Input), Normalise(Expected));
397	}
398
399	fn AssertUnchanged(Input:&str) {
400		let Opts = crate::Eliminate::Definition::Options::default();
401
402		let Result = crate::Eliminate::Transform::Run(Input, &Opts).expect("transform");
403
404		assert!(Result.is_none(), "expected no change but got:\n{}", Result.unwrap());
405	}
406
407	// --- Simple inline tests ------------------------------------------------
408
409	#[test]
410	fn SimpleInline() {
411		AssertEliminates(
412			r#"fn f() { let X = 5; println!("{}", X); }"#,
413			r#"fn f() { println!("{}", 5); }"#,
414		);
415	}
416
417	#[test]
418	fn ChainInline() { AssertEliminates("fn f() { let A = 1; let B = A + 1; g(B); }", "fn f() { g(1 + 1); }"); }
419
420	#[test]
421	fn BinaryExprParens() {
422		AssertEliminates("fn f() { let X = A + B; let _ = Y * X; }", "fn f() { let _ = Y * (A + B); }");
423	}
424
425	#[test]
426	fn BinaryExprNoParensInFnArg() { AssertEliminates("fn f() { let X = A + B; foo(X); }", "fn f() { foo(A + B); }"); }
427
428	#[test]
429	fn QuestionMarkInlined() {
430		AssertEliminates(
431			"async fn f() -> Result<(), E> { let X = foo().map_err(|e| e)?; bar(X); Ok(()) }",
432			"async fn f() -> Result<(), E> { bar(foo().map_err(|e| e)?); Ok(()) }",
433		);
434	}
435
436	// --- Macro substitution tests -------------------------------------------
437
438	#[test]
439	fn InlineIntoJsonMacro() {
440		AssertEliminates(
441			r#"fn f() {
442                let DataString = compute_data();
443                emit(json!({ "data": DataString }));
444            }"#,
445			r#"fn f() {
446                emit(json!({ "data": compute_data() }));
447            }"#,
448		);
449	}
450
451	#[test]
452	fn MacroAndExprMultiUseKept() {
453		AssertUnchanged(
454			r#"fn f() {
455                let URI = compute_uri();
456                dev_log!("{}", URI);
457                let _ = Url::parse(URI);
458            }"#,
459		);
460	}
461
462	#[test]
463	fn MacroDoubleUseKept() {
464		AssertUnchanged(
465			r#"fn f() {
466                let X = val();
467                emit(json!({ "a": X, "b": X }));
468            }"#,
469		);
470	}
471
472	// --- Loop-body tests (#58) ----------------------------------------------
473
474	/// Binding used only inside a for loop body must not be inlined.
475	/// Inlining would move the initialiser inside the loop, running it
476	/// N times instead of once and potentially changing semantics or
477	/// introducing a compile error for move-only types.
478	#[test]
479	fn LoopBodyBindingKept() {
480		AssertUnchanged(
481			r#"fn f() {
482                let X = expensive();
483                for item in &collection { process(item, X); }
484            }"#,
485		);
486	}
487
488	#[test]
489	fn WhileBodyBindingKept() {
490		AssertUnchanged(
491			r#"fn f() {
492                let X = expensive();
493                while cond { process(X); }
494            }"#,
495		);
496	}
497
498	#[test]
499	fn LoopExprBindingKept() {
500		AssertUnchanged(
501			r#"fn f() {
502                let X = expensive();
503                loop { process(X); break; }
504            }"#,
505		);
506	}
507
508	/// A binding used outside any loop must still be inlined normally.
509	#[test]
510	fn OutsideLoopStillInlined() {
511		AssertEliminates("fn f() { let X = compute(); g(X); }", "fn f() { g(compute()); }");
512	}
513
514	// --- Free-variable move-safety tests (regression for #56) ---------------
515
516	/// display = path.clone() must NOT be inlined when path is moved into a
517	/// struct field between the declaration and the devlog use site.
518	/// This is the exact pattern from AirClient::get_file_info that produced
519	/// E0382 after eliminate ran on Source/Air.
520	#[test]
521	fn DisplayCloneKeptWhenOriginalMovedFirst() {
522		AssertUnchanged(
523			r#"
524				pub async fn get_file_info(request_id: String, path: String) -> Result<(), E> {
525					let path_display = path.clone();
526
527					client
528						.get_file_info(Request::new(FileInfoRequest { request_id, path }))
529						.await?;
530
531					devlog!("{}", path_display, path.clone());
532
533					Ok(())
534				}
535			"#,
536		);
537	}
538
539	/// Same pattern with section instead of path
540	/// (AirClient::get_configuration).
541	#[test]
542	fn SectionDisplayCloneKeptWhenOriginalMovedFirst() {
543		AssertUnchanged(
544			r#"
545				pub async fn get_configuration(request_id: String, section: String) -> Result<(), E> {
546					let section_display = section.clone();
547
548					client
549						.get_configuration(Request::new(ConfigurationRequest { request_id, section }))
550						.await?;
551
552					devlog!("{}", section_display, section.clone());
553
554					Ok(())
555				}
556			"#,
557		);
558	}
559
560	/// When path is NOT moved between decl and use, the clone helper should
561	/// still be inlined (no false positive that would block legitimate
562	/// inlines).
563	#[test]
564	fn DisplayCloneInlinedWhenOriginalNotMoved() {
565		AssertEliminates(
566			r#"
567				pub async fn log_path(path: String) -> Result<(), E> {
568					let path_display = path.clone();
569
570					devlog!("{}", path_display);
571
572					Ok(())
573				}
574			"#,
575			r#"
576				pub async fn log_path(path: String) -> Result<(), E> {
577					devlog!("{}", path.clone());
578
579					Ok(())
580				}
581			"#,
582		);
583	}
584
585	// --- URL-parsing pattern ------------------------------------------------
586
587	#[test]
588	fn UrlPatternInlined() {
589		let Input = r#"
590            pub async fn Fn(
591                Service: &CocoonServiceImpl,
592
593                Request: ProvideCodeLensesRequest,
594            ) -> Result<Response<ProvideCodeLensesResponse>, Status> {
595                let URI = Request.uri.as_ref().map(|U| U.value.as_str()).unwrap_or("");
596
597                let DocumentURI = Url::parse(URI)
598                    .map_err(|E| Status::invalid_argument(format!("Invalid URI: {}", E)))?;
599
600                match Service.environment.ProvideCodeLenses(DocumentURI).await {
601                    Ok(_) => Ok(Response::new(ProvideCodeLensesResponse::default())),
602
603                    Err(Error) => Err(Status::internal(format!("Code lenses failed: {}", Error))),
604                }
605            }
606
607        "#;
608
609		let Expected = r#"
610            pub async fn Fn(
611                Service: &CocoonServiceImpl,
612
613                Request: ProvideCodeLensesRequest,
614            ) -> Result<Response<ProvideCodeLensesResponse>, Status> {
615                match Service
616                    .environment
617                    .ProvideCodeLenses(
618                        Url::parse(
619                            Request.uri.as_ref().map(|U| U.value.as_str()).unwrap_or(""),
620                        )
621                        .map_err(|E| Status::invalid_argument(format!("Invalid URI: {}", E)))?,
622                    )
623                    .await
624                {
625                    Ok(_) => Ok(Response::new(ProvideCodeLensesResponse::default())),
626
627                    Err(Error) => Err(Status::internal(format!("Code lenses failed: {}", Error))),
628                }
629            }
630
631        "#;
632
633		let Got = Transform(Input);
634
635		let Norm = Normalise(Expected);
636
637		assert_eq!(Got, Norm, "URL pattern not inlined as expected");
638	}
639
640	// --- Kept-as-is tests ---------------------------------------------------
641
642	#[test]
643	fn MultiUseKept() { AssertUnchanged("fn f() { let X = foo(); bar(X); baz(X); }"); }
644
645	#[test]
646	fn MutKept() { AssertUnchanged("fn f() { let mut X = 5; X += 1; g(X); }"); }
647
648	#[test]
649	fn ClosureCaptureKept() {
650		AssertEliminates(
651			"fn f() { let X = heavy(); let F = move || X; call(F); }",
652			"fn f() { let X = heavy(); call(move || X); }",
653		);
654	}
655
656	// --- Idempotency --------------------------------------------------------
657
658	#[test]
659	fn Idempotent() {
660		let Opts = crate::Eliminate::Definition::Options::default();
661
662		let Src = r#"fn f() { let X = 5; println!("{}", X); }"#;
663
664		let First = crate::Eliminate::Transform::Run(Src, &Opts)
665			.unwrap()
666			.expect("first pass should change");
667
668		let Second = crate::Eliminate::Transform::Run(&First, &Opts).unwrap();
669
670		assert!(Second.is_none(), "second pass must be a no-op:\n{}", First);
671	}
672}