Skip to main content

Maintain/Eliminate/Transform/
Count.rs

1//=============================================================================//
2// File Path: Element/Maintain/Source/Eliminate/Transform/Count.rs
3//=============================================================================//
4// Module: Count - Reference counting for let-binding candidates
5//
6// Counts how many times a named identifier is referenced in a slice of
7// statements, respecting:
8//   - Top-level shadowing: a second `let <target> = ...` at the same scope
9//     depth stops the count.
10//   - Inner-block shadowing: if an inner block re-introduces the name, all
11//     references inside that block are excluded (conservative).
12//   - Macro token streams: identifiers inside json!(), dev_log!(), and other
13//     macro invocations are counted via raw token-tree scanning.
14//   - Closure captures: references inside a closure body set InClosure.
15//   - Loop bodies: references inside for/while/loop bodies set InLoop. Callers
16//     treat such bindings as non-inlinable because inlining would move the
17//     initialiser expression inside the loop, changing evaluation semantics
18//     (runs N times instead of once).
19//=============================================================================//
20
21use proc_macro2::{TokenStream, TokenTree};
22use syn::{
23	Pat,
24	Stmt,
25	visit::{Visit, visit_block, visit_expr_closure, visit_expr_for_loop, visit_expr_loop, visit_expr_while},
26};
27
28// ---------------------------------------------------------------------------
29// Public API
30// ---------------------------------------------------------------------------
31
32/// Count references to Target in Stmts.
33///
34/// Returns (count, in_closure, in_loop).
35///
36/// - count      : number of times the identifier is referenced (plain
37///   expressions and macro token streams).
38/// - in_closure : true when at least one reference occurs inside a closure body
39///   (even if count == 1).
40/// - in_loop    : true when at least one reference occurs inside the body of a
41///   for, while, or loop expression (even if count == 1).
42///
43/// Counting stops when a top-level `let <target> = ...` shadow is encountered.
44pub fn CountReferences(Target:&str, Stmts:&[Stmt]) -> (usize, bool, bool) {
45	let mut TotalCount = 0usize;
46
47	let mut InClosure = false;
48
49	let mut InLoop = false;
50
51	for Stmt in Stmts {
52		if IsTopLevelShadow(Stmt, Target) {
53			break;
54		}
55
56		let mut Counter = ExprCounter { Target, Count:0, InClosure:false, InLoop:false, InsideLoop:false };
57
58		Counter.visit_stmt(Stmt);
59
60		TotalCount += Counter.Count;
61
62		if Counter.InClosure {
63			InClosure = true;
64		}
65
66		if Counter.InLoop {
67			InLoop = true;
68		}
69	}
70
71	(TotalCount, InClosure, InLoop)
72}
73
74// ---------------------------------------------------------------------------
75// Internals
76// ---------------------------------------------------------------------------
77
78fn IsTopLevelShadow(Stmt:&Stmt, Target:&str) -> bool {
79	if let Stmt::Local(Local) = Stmt {
80		if let Pat::Ident(P) = &Local.pat {
81			return P.ident == Target;
82		}
83	}
84
85	false
86}
87
88struct ExprCounter<'a> {
89	Target:&'a str,
90
91	Count:usize,
92
93	/// True when a reference to Target was found inside a closure body.
94	InClosure:bool,
95
96	/// True when a reference to Target was found inside a loop body.
97	InLoop:bool,
98
99	/// Internal flag: true when the visitor is currently descending inside
100	/// a for/while/loop body.
101	InsideLoop:bool,
102}
103
104impl<'ast> Visit<'ast> for ExprCounter<'ast> {
105	fn visit_expr_path(&mut self, Node:&'ast syn::ExprPath) {
106		if let Some(Ident) = Node.path.get_ident() {
107			if Ident == self.Target {
108				self.Count += 1;
109
110				if self.InsideLoop {
111					self.InLoop = true;
112				}
113			}
114		}
115	}
116
117	fn visit_expr_macro(&mut self, Node:&'ast syn::ExprMacro) {
118		let Found = CountIdentsInTokenStream(&Node.mac.tokens, self.Target);
119
120		if Found > 0 {
121			self.Count += Found;
122
123			if self.InsideLoop {
124				self.InLoop = true;
125			}
126		}
127	}
128
129	fn visit_stmt_macro(&mut self, Node:&'ast syn::StmtMacro) {
130		let Found = CountIdentsInTokenStream(&Node.mac.tokens, self.Target);
131
132		if Found > 0 {
133			self.Count += Found;
134
135			if self.InsideLoop {
136				self.InLoop = true;
137			}
138		}
139	}
140
141	fn visit_block(&mut self, Node:&'ast syn::Block) {
142		if BlockShadowsTarget(&Node.stmts, self.Target) {
143			return;
144		}
145
146		visit_block(self, Node);
147	}
148
149	fn visit_expr_closure(&mut self, Node:&'ast syn::ExprClosure) {
150		if ClosureParamShadows(Node, self.Target) {
151			return;
152		}
153
154		let CountBefore = self.Count;
155
156		visit_expr_closure(self, Node);
157
158		if self.Count > CountBefore {
159			self.InClosure = true;
160		}
161	}
162
163	// Set InsideLoop before descending into the three loop-body variants.
164
165	fn visit_expr_for_loop(&mut self, Node:&'ast syn::ExprForLoop) {
166		let Saved = self.InsideLoop;
167
168		self.InsideLoop = true;
169
170		visit_expr_for_loop(self, Node);
171
172		self.InsideLoop = Saved;
173	}
174
175	fn visit_expr_while(&mut self, Node:&'ast syn::ExprWhile) {
176		let Saved = self.InsideLoop;
177
178		self.InsideLoop = true;
179
180		visit_expr_while(self, Node);
181
182		self.InsideLoop = Saved;
183	}
184
185	fn visit_expr_loop(&mut self, Node:&'ast syn::ExprLoop) {
186		let Saved = self.InsideLoop;
187
188		self.InsideLoop = true;
189
190		visit_expr_loop(self, Node);
191
192		self.InsideLoop = Saved;
193	}
194}
195
196pub fn CountIdentsInTokenStream(Tokens:&TokenStream, Target:&str) -> usize {
197	let mut Count = 0;
198
199	for Tree in Tokens.clone() {
200		match Tree {
201			TokenTree::Ident(I) if I.to_string() == Target => Count += 1,
202
203			TokenTree::Group(G) => Count += CountIdentsInTokenStream(&G.stream(), Target),
204
205			TokenTree::Literal(Lit) => Count += CountIdentInFormatLiteral(&Lit.to_string(), Target),
206
207			_ => {},
208		}
209	}
210
211	Count
212}
213
214pub fn CountIdentInFormatLiteral(Lit:&str, Target:&str) -> usize {
215	if !Lit.starts_with('"') && !Lit.starts_with('r') {
216		return 0;
217	}
218
219	let Inner:&str = if Lit.starts_with('"') { &Lit[1..Lit.len().saturating_sub(1)] } else { Lit };
220
221	let SearchFor = format!("{{{Target}");
222
223	let mut Count = 0;
224
225	let Bytes = Inner.as_bytes();
226
227	let PatBytes = SearchFor.as_bytes();
228
229	let mut Pos = 0usize;
230
231	while Pos + PatBytes.len() <= Bytes.len() {
232		if Bytes[Pos..].starts_with(PatBytes) {
233			let IsEscaped = Pos > 0 && Bytes[Pos - 1] == b'{';
234
235			if !IsEscaped {
236				let After = &Bytes[Pos + PatBytes.len()..];
237
238				if After.first().map_or(false, |&B| B == b'}' || B == b':' || B == b'!') {
239					Count += 1;
240				}
241			}
242		}
243
244		Pos += 1;
245	}
246
247	Count
248}
249
250fn BlockShadowsTarget(Stmts:&[Stmt], Target:&str) -> bool { Stmts.iter().any(|S| IsTopLevelShadow(S, Target)) }
251
252fn ClosureParamShadows(Closure:&syn::ExprClosure, Target:&str) -> bool {
253	Closure
254		.inputs
255		.iter()
256		.any(|P| if let Pat::Ident(PIdent) = P { PIdent.ident == Target } else { false })
257}
258
259// ---------------------------------------------------------------------------
260// Unit tests
261// ---------------------------------------------------------------------------
262
263#[cfg(test)]
264mod Tests {
265
266	use super::*;
267
268	fn Stmts(Src:&str) -> Vec<Stmt> {
269		let File:syn::File = syn::parse_str(Src).expect("parse");
270
271		if let syn::Item::Fn(F) = &File.items[0] {
272			return F.block.stmts.clone();
273		}
274
275		vec![]
276	}
277
278	#[test]
279	fn SingleUse() {
280		let S = Stmts("fn f() { let X = 1; g(X); }");
281
282		let (Count, InClosure, InLoop) = CountReferences("X", &S[1..]);
283
284		assert_eq!(Count, 1);
285
286		assert!(!InClosure);
287
288		assert!(!InLoop);
289	}
290
291	#[test]
292	fn MultiUse() {
293		let S = Stmts("fn f() { let X = foo(); bar(X); baz(X); }");
294
295		let (Count, ..) = CountReferences("X", &S[1..]);
296
297		assert_eq!(Count, 2);
298	}
299
300	#[test]
301	fn ZeroUse() {
302		let S = Stmts("fn f() { let X = 1; g(1); }");
303
304		let (Count, ..) = CountReferences("X", &S[1..]);
305
306		assert_eq!(Count, 0);
307	}
308
309	#[test]
310	fn ShadowStops() {
311		let S = Stmts("fn f() { let X = 1; f(X); let X = 2; g(X); }");
312
313		let (Count, ..) = CountReferences("X", &S[1..]);
314
315		assert_eq!(Count, 1);
316	}
317
318	#[test]
319	fn MacroCountsAsUse() {
320		let S = Stmts(r#"fn f() { let URI = "x"; dev_log!("{}", URI); }"#);
321
322		let (Count, ..) = CountReferences("URI", &S[1..]);
323
324		assert_eq!(Count, 1);
325	}
326
327	#[test]
328	fn MacroAndExprBothCounted() {
329		let S = Stmts(
330			r#"fn f() {
331                let URI = "x";
332                dev_log!("{}", URI);
333                let _ = Url::parse(URI);
334            }"#,
335		);
336
337		let (Count, ..) = CountReferences("URI", &S[1..]);
338
339		assert_eq!(Count, 2, "URI used in macro + expression must count as 2");
340	}
341
342	#[test]
343	fn MacroOnlyUse() {
344		let S = Stmts(
345			r#"fn f() {
346                let DataString = compute();
347                emit(json!({ "data": DataString }));
348            }"#,
349		);
350
351		let (Count, ..) = CountReferences("DataString", &S[1..]);
352
353		assert_eq!(Count, 1);
354	}
355
356	#[test]
357	fn MacroDoubleUse() {
358		let S = Stmts(
359			r#"fn f() {
360                let X = val();
361                json!({ "a": X, "b": X });
362            }"#,
363		);
364
365		let (Count, ..) = CountReferences("X", &S[1..]);
366
367		assert_eq!(Count, 2);
368	}
369
370	#[test]
371	fn ClosureCaptureDetected() {
372		let S = Stmts("fn f() { let X = heavy(); let F = move || X; call(F); }");
373
374		let (Count, InClosure, _) = CountReferences("X", &S[1..]);
375
376		assert_eq!(Count, 1);
377
378		assert!(InClosure, "X used inside closure should set InClosure");
379	}
380
381	#[test]
382	fn InnerBlockShadowSkipped() {
383		let S = Stmts(
384			r#"fn f() {
385                let X = 1;
386                { let X = 2; g(X); }
387            }"#,
388		);
389
390		let (Count, ..) = CountReferences("X", &S[1..]);
391
392		assert_eq!(Count, 0);
393	}
394
395	#[test]
396	fn ClosureParamShadowSkipped() {
397		let S = Stmts("fn f() { let X = 5; let _ = |X| X + 1; g(0); }");
398
399		let (Count, InClosure, _) = CountReferences("X", &S[1..]);
400
401		assert_eq!(Count, 0);
402
403		assert!(!InClosure);
404	}
405
406	// Loop-body tests
407
408	/// X used only inside a for loop body: count=1 but InLoop=true.
409	/// The binding must not be inlined because that would move the initialiser
410	/// expression inside the loop, changing evaluation semantics.
411	#[test]
412	fn LoopBodySetsInLoop() {
413		let S = Stmts(
414			r#"fn f() {
415                let X = expensive();
416                for _ in &v { process(X); }
417            }"#,
418		);
419
420		let (Count, _, InLoop) = CountReferences("X", &S[1..]);
421
422		assert_eq!(Count, 1);
423
424		assert!(InLoop, "X used inside for body must set InLoop");
425	}
426
427	#[test]
428	fn WhileBodySetsInLoop() {
429		let S = Stmts(
430			r#"fn f() {
431                let X = expensive();
432                while cond { process(X); }
433            }"#,
434		);
435
436		let (Count, _, InLoop) = CountReferences("X", &S[1..]);
437
438		assert_eq!(Count, 1);
439
440		assert!(InLoop, "X used inside while body must set InLoop");
441	}
442
443	#[test]
444	fn LoopExprBodySetsInLoop() {
445		let S = Stmts(
446			r#"fn f() {
447                let X = expensive();
448                loop { process(X); break; }
449            }"#,
450		);
451
452		let (Count, _, InLoop) = CountReferences("X", &S[1..]);
453
454		assert_eq!(Count, 1);
455
456		assert!(InLoop, "X used inside loop body must set InLoop");
457	}
458
459	/// X used outside any loop: InLoop must be false.
460	#[test]
461	fn OutsideLoopNotFlagged() {
462		let S = Stmts("fn f() { let X = 1; g(X); }");
463
464		let (_, _, InLoop) = CountReferences("X", &S[1..]);
465
466		assert!(!InLoop);
467	}
468
469	/// X used both inside and outside a loop: count=2, InLoop=true.
470	/// Still ineligible (count != 1).
471	#[test]
472	fn UsedInsideAndOutsideLoop() {
473		let S = Stmts(
474			r#"fn f() {
475                let X = val();
476                g(X);
477                for _ in &v { h(X); }
478            }"#,
479		);
480
481		let (Count, _, InLoop) = CountReferences("X", &S[1..]);
482
483		assert_eq!(Count, 2);
484
485		assert!(InLoop);
486	}
487
488	// Format literal tests (unchanged)
489
490	#[test]
491	fn FormatLiteralBasicCapture() {
492		assert_eq!(CountIdentInFormatLiteral("\"{X}\"", "X"), 1);
493	}
494
495	#[test]
496	fn FormatLiteralWithSpec() {
497		assert_eq!(CountIdentInFormatLiteral("\"{X:.2}\"", "X"), 1);
498	}
499
500	#[test]
501	fn FormatLiteralWithAlt() {
502		assert_eq!(CountIdentInFormatLiteral("\"{X!r:}\"", "X"), 1);
503	}
504
505	#[test]
506	fn FormatLiteralTwoCaptures() {
507		assert_eq!(CountIdentInFormatLiteral("\"{X} and {X}\"", "X"), 2);
508	}
509
510	#[test]
511	fn FormatLiteralEscapedBraceNotCounted() {
512		assert_eq!(CountIdentInFormatLiteral("\"{{X}}\"", "X"), 0);
513	}
514
515	#[test]
516	fn FormatLiteralNumericNotCounted() {
517		assert_eq!(CountIdentInFormatLiteral("42", "X"), 0);
518	}
519
520	#[test]
521	fn FormatLiteralSubstringNotCounted() {
522		assert_eq!(CountIdentInFormatLiteral("\"{XY}\"", "X"), 0);
523	}
524
525	#[test]
526	fn ImplicitCaptureSingleUse() {
527		let S = Stmts(r#"fn f() { let X = 5; println!("{X}"); }"#);
528
529		let (Count, ..) = CountReferences("X", &S[1..]);
530
531		assert_eq!(Count, 1, "implicit capture {X} must count as one use");
532	}
533
534	#[test]
535	fn MixedImplicitAndExplicitCounts() {
536		let S = Stmts(
537			r#"fn f() {
538                let X = 5;
539                println!("{}", X);
540                println!("{X}");
541            }"#,
542		);
543
544		let (Count, ..) = CountReferences("X", &S[1..]);
545
546		assert_eq!(Count, 2, "old-style + implicit capture must count as 2");
547	}
548
549	#[test]
550	fn TwoImplicitCaptures() {
551		let S = Stmts(
552			r#"fn f() {
553                let X = 5;
554                log!("{X}");
555                log!("{X}");
556            }"#,
557		);
558
559		let (Count, ..) = CountReferences("X", &S[1..]);
560
561		assert_eq!(Count, 2);
562	}
563
564	#[test]
565	fn ImplicitCaptureWithPlainUseIsMulti() {
566		let S = Stmts(
567			r#"fn f() {
568                let URI = compute_uri();
569                log!("{URI}");
570                Url::parse(URI);
571            }"#,
572		);
573
574		let (Count, ..) = CountReferences("URI", &S[1..]);
575
576		assert_eq!(Count, 2);
577	}
578
579	#[test]
580	fn TwoSeparateStmtsMultiUse() {
581		let S = Stmts("fn f() { let X = foo(); bar(X); baz(X); }");
582
583		let (Count, ..) = CountReferences("X", &S[1..]);
584
585		assert_eq!(Count, 2);
586	}
587
588	#[test]
589	fn TwoUsesInSameCall() {
590		let S = Stmts("fn f() { let X = foo(); bar(X, X); }");
591
592		let (Count, ..) = CountReferences("X", &S[1..]);
593
594		assert_eq!(Count, 2);
595	}
596}