1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
2use std::env;
3use std::fs;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::sync::Mutex;
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use proc_macro2::{Delimiter, Span, TokenStream, TokenTree};
11use quote::ToTokens;
12use rustc_public::CompilerError;
13use rustc_public::crate_def::CrateDef;
14use rustc_public::mir::mono::{Instance, InstanceKind};
15use rustc_public::mir::{
16 AggregateKind, Body, CastKind, Operand, Place, PointerCoercion, ProjectionElem, Rvalue,
17 StatementKind, TerminatorKind,
18};
19use rustc_public::ty::{
20 AssocContainer, Binder, ConstantKind, ExistentialTraitRef, FnDef, GenericArgKind, GenericArgs,
21 ImplDef, RigidTy, TraitRef, Ty, TyKind, UintTy, VtblEntry,
22};
23use serde::{Deserialize, Serialize};
24use sha2::{Digest, Sha256};
25use syn::parse::Parser as SynParser;
26use syn::punctuated::Punctuated;
27use syn::spanned::Spanned;
28use syn::visit::{self, Visit};
29use syn::visit_mut::{self, VisitMut};
30use syn::{
31 Expr, ExprCall, ExprClosure, ExprMethodCall, FnArg, ImplItem, Item, ItemFn, ItemImpl,
32 ItemTrait, Macro, Pat, ReturnType, Signature, Token, TraitItem, Visibility,
33};
34
35use super::{FileContext, FrontendResult, LanguageBackend, ProjectContext};
36use crate::model::{
37 CallLabel, CallSite, CallSiteId, CallSyntax, CallTarget, DispatchCandidate, DispatchEvidence,
38 DispatchResolution, FileAnalysis, FunctionInfo, LanguageFact, LanguageId, SourceSpan, SymbolId,
39 UnresolvedReason,
40};
41use crate::source::{
42 collect_source_files, contains_generated_component, is_generated_directory, paths_match,
43};
44
45#[derive(Default)]
46pub struct RustBackend;
47
48pub static RUST_BACKEND: RustBackend = RustBackend;
49
50static RUSTC_DRIVER_LOCK: Mutex<()> = Mutex::new(());
51static TEMP_SOURCE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
52const RUSTC_CAPTURE_DIRECTORY: &str = "DIFFKIT_RUSTC_CAPTURE_DIRECTORY";
53const RUSTC_ANALYSIS_ROOT: &str = "DIFFKIT_RUSTC_ANALYSIS_ROOT";
54const SNAPSHOT_FINGERPRINT_FILE: &str = ".diffkit-snapshot-key";
55const RUST_SEMANTIC_CACHE_SCHEMA: u32 = 9;
56
57pub fn analyze_semantic_file(path: &Path) -> FrontendResult<FileAnalysis> {
63 analyze_semantic_file_with_entries(path, &[])
64}
65
66pub fn analyze_semantic_file_with_entries(
67 path: &Path,
68 entries: &[String],
69) -> FrontendResult<FileAnalysis> {
70 if !path.is_file() {
71 return Err(std::io::Error::other(format!(
72 "Rust analysis currently requires a standalone .rs file: {}",
73 path.display()
74 ))
75 .into());
76 }
77
78 let source = fs::read_to_string(path)?;
79 let syntax = RustBackend.analyze_file(&FileContext { path, module: &[] }, &source)?;
80 let semantic = collect_rustc_program(path)?;
81 let analysis = merge_semantic_program(syntax.clone(), semantic);
82 let missing_entries = entries
83 .iter()
84 .filter(|entry| entry.contains('<') && !analysis_has_entry(&analysis, entry))
85 .cloned()
86 .collect::<Vec<_>>();
87 if missing_entries.is_empty() {
88 return Ok(analysis);
89 }
90
91 let Some(seeded_source) = append_entry_seeds(&source, &missing_entries)? else {
92 return Ok(analysis);
93 };
94 let temporary = TemporaryRustSource::create(&seeded_source)?;
95 let temporary_path = temporary.path();
96 let mut semantic = collect_rustc_program(&temporary_path)?;
97 remap_semantic_file(&mut semantic, &temporary_path, path);
98 Ok(merge_semantic_program(syntax, semantic))
99}
100
101fn remap_semantic_file(program: &mut SemanticProgram, from: &Path, to: &Path) {
102 for function in &mut program.functions {
103 if source_files_match(&function.body_span.file, from) {
104 function.body_span.file = to.to_path_buf();
105 }
106 for call in &mut function.calls {
107 if source_files_match(&call.span.file, from) {
108 call.span.file = to.to_path_buf();
109 }
110 }
111 }
112}
113
114pub fn analyze_semantic_source(source: &str, entries: &[String]) -> FrontendResult<FileAnalysis> {
115 let temporary = TemporaryRustSource::create(source)?;
116 analyze_semantic_file_with_entries(&temporary.path(), entries)
117}
118
119pub fn rustc_wrapper_requested() -> bool {
122 env::var_os(RUSTC_CAPTURE_DIRECTORY).is_some()
123}
124
125pub fn run_rustc_wrapper() -> FrontendResult<()> {
129 let mut arguments = env::args_os().skip(1);
130 let rustc = arguments
131 .next()
132 .ok_or_else(|| std::io::Error::other("rustc wrapper did not receive a compiler path"))?;
133 let arguments = arguments.collect::<Vec<_>>();
134 let mut driver_arguments = vec![rustc.to_string_lossy().into_owned()];
135 driver_arguments.extend(
136 arguments
137 .iter()
138 .cloned()
139 .map(|argument| {
140 argument
141 .into_string()
142 .map_err(|_| std::io::Error::other("rustc argument is not valid UTF-8"))
143 })
144 .collect::<Result<Vec<_>, _>>()?,
145 );
146
147 let program = match rustc_public::run_with_tcx!(&driver_arguments, |tcx| {
148 if tcx.dcx().has_errors().is_some() {
149 std::ops::ControlFlow::<(), SemanticProgram>::Break(())
150 } else {
151 let (program, expanding_instantiation) = collect_instances();
152 if expanding_instantiation {
153 std::ops::ControlFlow::Break(())
154 } else {
155 std::ops::ControlFlow::Continue(program)
156 }
157 }
158 }) {
159 Ok(program) => program,
160 Err(CompilerError::Failed) => {
161 return Err(std::io::Error::other("rustc failed during semantic capture").into());
162 }
163 Err(CompilerError::Skipped) => return Ok(()),
164 Err(CompilerError::Interrupted(())) => {
165 return Err(std::io::Error::other("rustc semantic capture was interrupted").into());
166 }
167 };
168
169 persist_semantic_program(&program)
170}
171
172fn persist_semantic_program(program: &SemanticProgram) -> FrontendResult<()> {
173 let directory = PathBuf::from(env::var_os(RUSTC_CAPTURE_DIRECTORY).ok_or_else(|| {
174 std::io::Error::other("semantic capture directory disappeared from the environment")
175 })?);
176 fs::create_dir_all(&directory)?;
177 let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
178 let destination = directory.join(format!("{}-{sequence}.json", std::process::id()));
179 let temporary = destination.with_extension("json.tmp");
180 fs::write(&temporary, serde_json::to_vec(program)?)?;
181 fs::rename(temporary, destination)?;
182 Ok(())
183}
184
185pub(crate) fn analyze_semantic_project(
189 project_root: &Path,
190 wrapper_executable: &Path,
191 entries: &[String],
192 session: &RustProjectSession,
193) -> FrontendResult<FileAnalysis> {
194 let root = project_root.canonicalize()?;
195 if !root.join("Cargo.toml").is_file() {
196 return Err(std::io::Error::other(format!(
197 "no Cargo.toml at Rust project root {}",
198 root.display()
199 ))
200 .into());
201 }
202 let generic_entries = entries
203 .iter()
204 .filter(|entry| entry.contains('<'))
205 .cloned()
206 .collect::<Vec<_>>();
207 if generic_entries.is_empty() {
208 return capture_semantic_project(&root, wrapper_executable, session);
209 }
210
211 let seeded = SeededRustProject::create(&root)?;
212 seed_project_entries(seeded.root(), &generic_entries)?;
213 let mut analysis = capture_semantic_project(seeded.root(), wrapper_executable, session)?;
214 remap_analysis_root(&mut analysis, seeded.root(), &root);
215 remove_seed_functions(&mut analysis);
216 Ok(analysis)
217}
218
219fn capture_semantic_project(
220 root: &Path,
221 wrapper_executable: &Path,
222 session: &RustProjectSession,
223) -> FrontendResult<FileAnalysis> {
224 let started = std::time::Instant::now();
225 let fingerprint_started = std::time::Instant::now();
226 let fingerprint = session
227 .cache_path()
228 .map(|_| semantic_cache_fingerprint(root))
229 .transpose()?;
230 let fingerprint_elapsed = fingerprint_started.elapsed();
231 let cache_lock = fs::OpenOptions::new()
236 .create(true)
237 .read(true)
238 .write(true)
239 .truncate(false)
240 .open(session.target().join(".diffkit.lock"))?;
241 cache_lock.lock()?;
242 if let (Some(cache_path), Some(fingerprint)) = (session.cache_path(), fingerprint.as_deref())
243 && let Some(analysis) = read_analysis_cache(cache_path, fingerprint, root)
244 {
245 session.report_cache(true, fingerprint, fingerprint_elapsed, started.elapsed());
246 return Ok(analysis);
247 }
248 let capture = RustProjectCapture::create()?;
249 let wrapper = wrapper_executable.canonicalize()?;
250 let clean = Command::new("cargo")
251 .args(["clean", "--workspace", "--quiet"])
252 .current_dir(root)
253 .env("CARGO_TARGET_DIR", session.target())
254 .output()?;
255 if !clean.status.success() {
256 let diagnostic = String::from_utf8_lossy(&clean.stderr).trim().to_owned();
257 return Err(std::io::Error::other(if diagnostic.is_empty() {
258 format!("Cargo cache preparation exited with {}", clean.status)
259 } else {
260 diagnostic
261 })
262 .into());
263 }
264 let mut check = Command::new("cargo");
265 check.args([
266 "check",
267 "--workspace",
268 "--all-targets",
269 "--all-features",
270 "--quiet",
271 ]);
272 let cargo_jobs = session.cargo_jobs.map(|jobs| jobs.to_string());
273 if let Some(jobs) = &cargo_jobs {
274 check.args(["--jobs", jobs]);
275 }
276 let output = check
277 .current_dir(root)
278 .env("RUSTC_WORKSPACE_WRAPPER", &wrapper)
279 .env(RUSTC_CAPTURE_DIRECTORY, capture.results())
280 .env(RUSTC_ANALYSIS_ROOT, root)
281 .env("CARGO_TARGET_DIR", session.target())
282 .env("CARGO_INCREMENTAL", "0")
286 .output()?;
287 if !output.status.success() {
288 let diagnostic = String::from_utf8_lossy(&output.stderr).trim().to_owned();
289 return Err(std::io::Error::other(if diagnostic.is_empty() {
290 format!("Cargo semantic analysis exited with {}", output.status)
291 } else {
292 diagnostic
293 })
294 .into());
295 }
296
297 let mut result_files = fs::read_dir(capture.results())?.collect::<Result<Vec<_>, _>>()?;
298 result_files.sort_by_key(std::fs::DirEntry::path);
299 let mut semantic = SemanticProgram {
300 crate_name: String::new(),
301 functions: Vec::new(),
302 };
303 for result_file in result_files {
304 if result_file
305 .path()
306 .extension()
307 .and_then(|value| value.to_str())
308 != Some("json")
309 {
310 continue;
311 }
312 let mut program: SemanticProgram = serde_json::from_slice(&fs::read(result_file.path())?)?;
313 semantic.functions.append(&mut program.functions);
314 }
315 if semantic.functions.is_empty() {
316 return Err(std::io::Error::other(format!(
317 "Cargo produced no Rust semantic targets under {}",
318 root.display()
319 ))
320 .into());
321 }
322 semantic.functions.sort_by(|left, right| {
323 (&left.key, &left.body_span.file, left.body_span.start_line).cmp(&(
324 &right.key,
325 &right.body_span.file,
326 right.body_span.start_line,
327 ))
328 });
329 semantic.functions.dedup_by(|left, right| {
330 left.key == right.key
331 && left.body_span.file == right.body_span.file
332 && left.body_span.start_line == right.body_span.start_line
333 && left.body_span.start_column == right.body_span.start_column
334 });
335 respecialize_merged_semantic_program(&mut semantic);
336
337 let syntax = analyze_project_sources(root)?;
338 let analysis = deduplicate_analysis(merge_semantic_program(syntax, semantic));
339 if let (Some(cache_path), Some(fingerprint)) = (session.cache_path(), fingerprint) {
340 let mut cache = CachedRustAnalysis {
341 schema: RUST_SEMANTIC_CACHE_SCHEMA,
342 fingerprint,
343 analysis,
344 };
345 relativize_analysis_root(&mut cache.analysis, root);
346 let _ = write_analysis_cache(cache_path, &cache);
347 materialize_analysis_root(&mut cache.analysis, root);
348 session.report_cache(
349 false,
350 &cache.fingerprint,
351 fingerprint_elapsed,
352 started.elapsed(),
353 );
354 return Ok(cache.analysis);
355 }
356 Ok(analysis)
357}
358
359fn semantic_cache_fingerprint(root: &Path) -> std::io::Result<String> {
360 let mut hasher = Sha256::new();
361 hash_cache_bytes(&mut hasher, &RUST_SEMANTIC_CACHE_SCHEMA.to_le_bytes());
362 hash_cache_bytes(&mut hasher, &analyzer_identity());
363 hash_command_identity(&mut hasher, root, "rustc", &["-vV"])?;
364 hash_command_identity(&mut hasher, root, "cargo", &["-V"])?;
365
366 let mut environment = env::vars_os()
367 .filter(|(key, _)| rust_build_environment_key(key))
368 .collect::<Vec<_>>();
369 environment.sort_by(|left, right| {
370 left.0
371 .as_encoded_bytes()
372 .cmp(right.0.as_encoded_bytes())
373 .then_with(|| left.1.as_encoded_bytes().cmp(right.1.as_encoded_bytes()))
374 });
375 for (key, value) in environment {
376 hash_cache_bytes(&mut hasher, key.as_encoded_bytes());
377 hash_cache_bytes(&mut hasher, value.as_encoded_bytes());
378 }
379 if let Ok(snapshot_key) = fs::read(root.join(SNAPSHOT_FINGERPRINT_FILE)) {
380 hash_cache_bytes(&mut hasher, b"git-snapshot");
381 hash_cache_bytes(&mut hasher, &snapshot_key);
382 } else {
383 hash_project_inputs(root, root, &mut hasher)?;
384 }
385 Ok(format!("{:x}", hasher.finalize()))
386}
387
388fn rust_build_environment_key(key: &std::ffi::OsStr) -> bool {
389 let Some(key) = key.to_str() else {
390 return true;
391 };
392 matches!(
393 key,
394 "PATH"
395 | "HOME"
396 | "RUSTFLAGS"
397 | "RUSTDOCFLAGS"
398 | "RUSTC"
399 | "RUSTUP_HOME"
400 | "RUSTUP_TOOLCHAIN"
401 | "CARGO_HOME"
402 | "CARGO_BUILD_TARGET"
403 | "CARGO_ENCODED_RUSTFLAGS"
404 | "CC"
405 | "CXX"
406 | "AR"
407 | "CFLAGS"
408 | "CXXFLAGS"
409 | "LDFLAGS"
410 | "MACOSX_DEPLOYMENT_TARGET"
411 | "SDKROOT"
412 | "PKG_CONFIG_PATH"
413 ) || [
414 "CARGO_PROFILE_",
415 "CARGO_TARGET_",
416 "RUSTC_",
417 "CC_",
418 "CXX_",
419 "AR_",
420 "CFLAGS_",
421 "CXXFLAGS_",
422 "PKG_CONFIG_",
423 ]
424 .iter()
425 .any(|prefix| key.starts_with(prefix))
426}
427
428fn analyzer_identity() -> Vec<u8> {
429 format!(
433 "{}:semantic-schema-{RUST_SEMANTIC_CACHE_SCHEMA}",
434 env!("CARGO_PKG_VERSION")
435 )
436 .into_bytes()
437}
438
439fn hash_command_identity(
440 hasher: &mut Sha256,
441 root: &Path,
442 program: &str,
443 arguments: &[&str],
444) -> std::io::Result<()> {
445 let output = Command::new(program)
446 .args(arguments)
447 .current_dir(root)
448 .output()?;
449 hash_cache_bytes(hasher, program.as_bytes());
450 hash_cache_bytes(hasher, &output.stdout);
451 hash_cache_bytes(hasher, &output.stderr);
452 hash_cache_bytes(hasher, &output.status.code().unwrap_or(-1).to_le_bytes());
453 Ok(())
454}
455
456fn hash_project_inputs(root: &Path, directory: &Path, hasher: &mut Sha256) -> std::io::Result<()> {
457 let mut entries = fs::read_dir(directory)?.collect::<Result<Vec<_>, _>>()?;
458 entries.sort_by_key(std::fs::DirEntry::path);
459 for entry in entries {
460 let path = entry.path();
461 let file_type = entry.file_type()?;
462 if file_type.is_dir() {
463 if !is_generated_directory(&path) {
464 hash_project_inputs(root, &path, hasher)?;
465 }
466 continue;
467 }
468
469 let relative = path.strip_prefix(root).unwrap_or(&path);
470 if file_type.is_file() {
471 hash_cache_bytes(hasher, b"file");
472 hash_cache_bytes(hasher, relative.as_os_str().as_encoded_bytes());
473 hash_file_contents(&path, hasher)?;
474 } else if file_type.is_symlink() {
475 hash_cache_bytes(hasher, b"symlink");
476 hash_cache_bytes(hasher, relative.as_os_str().as_encoded_bytes());
477 let target = fs::read_link(&path)?;
478 hash_cache_bytes(hasher, target.as_os_str().as_encoded_bytes());
479 if let Ok(resolved) = path.canonicalize()
480 && resolved.is_file()
481 {
482 hash_file_contents(&resolved, hasher)?;
483 }
484 }
485 }
486 Ok(())
487}
488
489fn hash_file_contents(path: &Path, hasher: &mut Sha256) -> std::io::Result<()> {
490 let mut file = fs::File::open(path)?;
491 let length = file.metadata()?.len();
492 hasher.update(length.to_le_bytes());
493 let mut buffer = [0u8; 64 * 1024];
494 loop {
495 let read = file.read(&mut buffer)?;
496 if read == 0 {
497 break;
498 }
499 hasher.update(&buffer[..read]);
500 }
501 Ok(())
502}
503
504fn hash_cache_bytes(hasher: &mut Sha256, bytes: &[u8]) {
505 hasher.update((bytes.len() as u64).to_le_bytes());
506 hasher.update(bytes);
507}
508
509fn read_analysis_cache(path: PathBuf, fingerprint: &str, root: &Path) -> Option<FileAnalysis> {
510 let mut cached = serde_json::from_slice::<CachedRustAnalysis>(&fs::read(path).ok()?).ok()?;
511 if cached.schema != RUST_SEMANTIC_CACHE_SCHEMA || cached.fingerprint != fingerprint {
512 return None;
513 }
514 materialize_analysis_root(&mut cached.analysis, root);
515 Some(cached.analysis)
516}
517
518fn write_analysis_cache(path: PathBuf, cached: &CachedRustAnalysis) -> std::io::Result<()> {
519 if let Some(parent) = path.parent() {
520 fs::create_dir_all(parent)?;
521 }
522 let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
523 let temporary = path.with_extension(format!("json.tmp-{}-{sequence}", std::process::id()));
524 fs::write(&temporary, serde_json::to_vec(cached)?)?;
525 if let Err(error) = fs::rename(&temporary, &path) {
526 if path.is_file() {
527 fs::remove_file(&path)?;
528 fs::rename(&temporary, &path)?;
529 } else {
530 let _ = fs::remove_file(temporary);
531 return Err(error);
532 }
533 }
534 Ok(())
535}
536
537struct RustProjectCapture {
538 directory: PathBuf,
539}
540
541impl RustProjectCapture {
542 fn create() -> std::io::Result<Self> {
543 let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
544 let directory = env::temp_dir().join(format!(
545 "diffkit-rust-capture-{}-{sequence}",
546 std::process::id()
547 ));
548 fs::create_dir(&directory)?;
549 fs::create_dir(directory.join("results"))?;
550 Ok(Self { directory })
551 }
552
553 fn results(&self) -> PathBuf {
554 self.directory.join("results")
555 }
556}
557
558impl Drop for RustProjectCapture {
559 fn drop(&mut self) {
560 let _ = fs::remove_dir_all(&self.directory);
561 }
562}
563
564pub(crate) struct RustProjectSession {
571 target: PathBuf,
572 cleanup: Option<PathBuf>,
573 endpoint: Option<String>,
574 verbose: bool,
575 cargo_jobs: Option<usize>,
576}
577
578impl RustProjectSession {
579 pub(crate) fn create() -> std::io::Result<Self> {
580 let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
581 let directory = env::temp_dir().join(format!(
582 "diffkit-rust-session-{}-{sequence}",
583 std::process::id()
584 ));
585 fs::create_dir(&directory)?;
586 Ok(Self {
587 target: directory.join("target"),
588 cleanup: Some(directory),
589 endpoint: None,
590 verbose: false,
591 cargo_jobs: None,
592 })
593 }
594
595 pub(crate) fn create_cached(
596 project_root: &Path,
597 endpoint: &str,
598 verbose: bool,
599 ) -> std::io::Result<Self> {
600 let mut components = Path::new(endpoint).components();
601 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
602 || components.next().is_some()
603 {
604 return Err(std::io::Error::new(
605 std::io::ErrorKind::InvalidInput,
606 "Rust semantic cache endpoint must be one path component",
607 ));
608 }
609 let target = project_root
610 .join("target")
611 .join("diffkit-semantic")
612 .join(endpoint);
613 fs::create_dir_all(&target)?;
614 Ok(Self {
615 target,
616 cleanup: None,
617 endpoint: Some(endpoint.to_owned()),
618 verbose,
619 cargo_jobs: None,
620 })
621 }
622
623 pub(crate) fn limit_cargo_jobs(&mut self, jobs: usize) {
624 if env::var_os("CARGO_BUILD_JOBS").is_none() {
625 self.cargo_jobs = Some(jobs.max(1));
626 }
627 }
628
629 fn target(&self) -> &Path {
630 &self.target
631 }
632
633 fn cache_path(&self) -> Option<PathBuf> {
634 self.cleanup
635 .is_none()
636 .then(|| self.target.join(".diffkit").join("analysis-v2.json"))
637 }
638
639 fn report_cache(
640 &self,
641 hit: bool,
642 fingerprint_key: &str,
643 fingerprint: std::time::Duration,
644 total: std::time::Duration,
645 ) {
646 if self.verbose {
647 let status = if hit { "hit" } else { "miss" };
648 eprintln!(
649 "Rust semantic cache {status}: {} [{}] ({} ms; fingerprint {} ms)",
650 self.endpoint.as_deref().unwrap_or("temporary"),
651 &fingerprint_key[..fingerprint_key.len().min(12)],
652 total.as_millis(),
653 fingerprint.as_millis(),
654 );
655 }
656 }
657}
658
659impl Drop for RustProjectSession {
660 fn drop(&mut self) {
661 if let Some(directory) = &self.cleanup {
662 let _ = fs::remove_dir_all(directory);
663 }
664 }
665}
666
667struct SeededRustProject {
668 directory: PathBuf,
669 root: PathBuf,
670}
671
672impl SeededRustProject {
673 fn create(source: &Path) -> std::io::Result<Self> {
674 let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
675 let directory = env::temp_dir().join(format!(
676 "diffkit-rust-seeded-{}-{sequence}",
677 std::process::id()
678 ));
679 let root = directory.join("project");
680 fs::create_dir_all(&root)?;
681 copy_rust_project(source, source, &root)?;
682 Ok(Self { directory, root })
683 }
684
685 fn root(&self) -> &Path {
686 &self.root
687 }
688}
689
690impl Drop for SeededRustProject {
691 fn drop(&mut self) {
692 let _ = fs::remove_dir_all(&self.directory);
693 }
694}
695
696fn copy_rust_project(
697 source_root: &Path,
698 directory: &Path,
699 destination: &Path,
700) -> std::io::Result<()> {
701 let mut entries = fs::read_dir(directory)?.collect::<Result<Vec<_>, _>>()?;
702 entries.sort_by_key(std::fs::DirEntry::path);
703 for entry in entries {
704 let path = entry.path();
705 let relative = path.strip_prefix(source_root).unwrap_or(&path);
706 if contains_generated_component(relative) {
707 continue;
708 }
709 let target = destination.join(relative);
710 let file_type = entry.file_type()?;
711 if file_type.is_dir() {
712 fs::create_dir_all(&target)?;
713 copy_rust_project(source_root, &path, destination)?;
714 } else if file_type.is_file() {
715 if let Some(parent) = target.parent() {
716 fs::create_dir_all(parent)?;
717 }
718 fs::copy(path, target)?;
719 } else if file_type.is_symlink() {
720 let resolved = fs::canonicalize(path)?;
721 if resolved.starts_with(source_root) && resolved.is_file() {
722 if let Some(parent) = target.parent() {
723 fs::create_dir_all(parent)?;
724 }
725 fs::copy(resolved, target)?;
726 }
727 }
728 }
729 Ok(())
730}
731
732fn seed_project_entries(root: &Path, entries: &[String]) -> FrontendResult<()> {
733 let files = collect_source_files(root, &["rs"])?;
734 for file in files {
735 let source = fs::read_to_string(&file)?;
736 if let Some(seeded) = append_entry_seeds(&source, entries)? {
737 fs::write(file, seeded)?;
738 }
739 }
740 Ok(())
741}
742
743fn remap_analysis_root(analysis: &mut FileAnalysis, from: &Path, to: &Path) {
744 transform_analysis_paths(analysis, |path| {
745 path.strip_prefix(from)
746 .ok()
747 .map(|relative| to.join(relative))
748 });
749}
750
751fn relativize_analysis_root(analysis: &mut FileAnalysis, root: &Path) {
752 transform_analysis_paths(analysis, |path| {
753 path.strip_prefix(root).ok().map(Path::to_path_buf)
754 });
755}
756
757fn materialize_analysis_root(analysis: &mut FileAnalysis, root: &Path) {
758 transform_analysis_paths(analysis, |path| path.is_relative().then(|| root.join(path)));
759}
760
761fn transform_analysis_paths(
762 analysis: &mut FileAnalysis,
763 mut transform: impl FnMut(&Path) -> Option<PathBuf>,
764) {
765 for function in &mut analysis.functions {
766 if let Some(path) = transform(&function.span.file) {
767 function.span.file = path;
768 }
769 for call in &mut function.calls {
770 if let Some(path) = transform(&call.span.file) {
771 call.span.file = path;
772 }
773 }
774 }
775 for fact in &mut analysis.facts {
776 if let Some(path) = transform(&fact.span.file) {
777 fact.span.file = path;
778 }
779 }
780 analysis.source_files = std::mem::take(&mut analysis.source_files)
781 .into_iter()
782 .map(|file| transform(&file).unwrap_or(file))
783 .collect();
784}
785
786fn remove_seed_functions(analysis: &mut FileAnalysis) {
787 let seeds = analysis
788 .functions
789 .iter()
790 .filter(|function| function.label.default.contains("__diffkit_seed_"))
791 .map(|function| function.id.clone())
792 .collect::<HashSet<_>>();
793 analysis
794 .functions
795 .retain(|function| !seeds.contains(&function.id));
796 analysis.facts.retain(|fact| !seeds.contains(&fact.subject));
797}
798
799fn analyze_project_sources(root: &Path) -> FrontendResult<FileAnalysis> {
800 let files = collect_source_files(root, &["rs"])?;
801 let mut combined = FileAnalysis::default();
802 for file in files {
803 let source = fs::read_to_string(&file)?;
804 let module = rust_module_path(root, &file);
805 let analysis = RustBackend.analyze_file(
806 &FileContext {
807 path: &file,
808 module: &module,
809 },
810 &source,
811 )?;
812 combined.append(analysis);
813 }
814 Ok(combined)
815}
816
817fn rust_module_path(root: &Path, file: &Path) -> Vec<String> {
818 let source_root = root.join("src");
819 let relative = file
820 .strip_prefix(&source_root)
821 .or_else(|_| file.strip_prefix(root))
822 .unwrap_or(file);
823 let mut parts = relative
824 .parent()
825 .into_iter()
826 .flat_map(Path::components)
827 .filter_map(|component| component.as_os_str().to_str().map(ToOwned::to_owned))
828 .collect::<Vec<_>>();
829 let stem = file
830 .file_stem()
831 .and_then(|stem| stem.to_str())
832 .unwrap_or_default();
833 if !matches!(stem, "lib" | "main" | "mod") {
834 parts.push(stem.to_owned());
835 }
836 parts
837}
838
839fn deduplicate_analysis(analysis: FileAnalysis) -> FileAnalysis {
840 let mut functions = BTreeMap::<SymbolId, FunctionInfo>::new();
841 for function in analysis.functions {
842 functions
843 .entry(function.id.clone())
844 .and_modify(|existing| {
845 for call in &function.calls {
846 if !existing.calls.contains(call) {
847 existing.calls.push(call.clone());
848 }
849 }
850 existing.calls.sort_by_key(|call| {
851 (
852 call.span.file.clone(),
853 call.span.start_line,
854 call.span.start_column,
855 )
856 });
857 })
858 .or_insert(function);
859 }
860 FileAnalysis {
861 functions: functions.into_values().collect(),
862 facts: analysis.facts,
863 source_files: analysis.source_files,
864 roots: analysis.roots,
865 }
866}
867
868fn analysis_has_entry(analysis: &FileAnalysis, entry: &str) -> bool {
869 let expected = entry.replace('.', "::").replace("::<", "<");
870 analysis.functions.iter().any(|function| {
871 let label = &function.label.default;
872 let callable =
873 outer_call_arguments_start(label).map_or(label.as_str(), |index| &label[..index]);
874 callable == expected || callable.ends_with(&format!("::{expected}"))
875 })
876}
877
878fn append_entry_seeds(source: &str, entries: &[String]) -> FrontendResult<Option<String>> {
879 let file = syn::parse_file(source)?;
880 let mut functions = Vec::new();
881 collect_seedable_functions(&file.items, &[], &mut functions);
882 let mut seeds = Vec::new();
883
884 for entry in entries {
885 let Some(generic_start) = entry.find('<') else {
886 continue;
887 };
888 if !entry.ends_with('>') || entry.starts_with('<') {
889 continue;
890 }
891 let base = entry[..generic_start]
892 .trim_end_matches("::")
893 .replace('.', "::");
894 let generic_arguments = &entry[generic_start + 1..entry.len() - 1];
895 let lookup = base.strip_prefix("crate::").unwrap_or(&base);
896 let mut matches = functions
897 .iter()
898 .filter(|function| {
899 lookup == function.qualified_name
900 || (!lookup.contains("::") && lookup == function.name)
901 })
902 .collect::<Vec<_>>();
903 if matches.len() != 1 {
904 continue;
905 }
906 let function = matches.pop().expect("one seedable function matched");
907 let arguments = (0..function.argument_count)
908 .map(|_| "::core::mem::MaybeUninit::uninit().assume_init()")
909 .collect::<Vec<_>>()
910 .join(", ");
911 seeds.push(format!(
912 "\n#[doc(hidden)]\n#[allow(dead_code, invalid_value, unused_unsafe)]\nfn __diffkit_seed_{}() {{\n unsafe {{ let _ = {}::<{}>({}); }}\n}}\n",
913 seeds.len(), function.qualified_name, generic_arguments, arguments
914 ));
915 }
916
917 if seeds.is_empty() {
918 Ok(None)
919 } else {
920 let mut seeded = source.to_owned();
921 seeded.extend(seeds);
922 Ok(Some(seeded))
923 }
924}
925
926struct SeedableFunction {
927 name: String,
928 qualified_name: String,
929 argument_count: usize,
930}
931
932fn collect_seedable_functions(
933 items: &[Item],
934 module: &[String],
935 functions: &mut Vec<SeedableFunction>,
936) {
937 for item in items {
938 match item {
939 Item::Fn(function) => {
940 let name = function.sig.ident.to_string();
941 let qualified_name = module
942 .iter()
943 .map(String::as_str)
944 .chain(std::iter::once(name.as_str()))
945 .collect::<Vec<_>>()
946 .join("::");
947 functions.push(SeedableFunction {
948 name,
949 qualified_name,
950 argument_count: function.sig.inputs.len(),
951 });
952 }
953 Item::Mod(item_mod) => {
954 if let Some((_, nested)) = &item_mod.content {
955 let mut nested_module = module.to_vec();
956 nested_module.push(item_mod.ident.to_string());
957 collect_seedable_functions(nested, &nested_module, functions);
958 }
959 }
960 _ => {}
961 }
962 }
963}
964
965impl TemporaryRustSource {
966 fn create(source: &str) -> std::io::Result<Self> {
967 let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
968 let directory = std::env::temp_dir().join(format!(
969 "diffkit-rust-source-{}-{sequence}",
970 std::process::id()
971 ));
972 fs::create_dir(&directory)?;
973 let temporary = TemporaryRustSource { directory };
974 let path = temporary.directory.join("input.rs");
975 fs::write(&path, source)?;
976 Ok(temporary)
977 }
978
979 fn path(&self) -> PathBuf {
980 self.directory.join("input.rs")
981 }
982}
983
984struct TemporaryRustSource {
985 directory: PathBuf,
986}
987
988impl Drop for TemporaryRustSource {
989 fn drop(&mut self) {
990 let _ = fs::remove_file(self.directory.join("input.rs"));
991 let _ = fs::remove_dir(&self.directory);
992 }
993}
994
995struct TemporaryRustOutput {
996 directory: PathBuf,
997}
998
999impl TemporaryRustOutput {
1000 fn create() -> std::io::Result<Self> {
1001 let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
1002 let directory = std::env::temp_dir().join(format!(
1003 "diffkit-rust-output-{}-{sequence}",
1004 std::process::id()
1005 ));
1006 fs::create_dir(&directory)?;
1007 Ok(Self { directory })
1008 }
1009}
1010
1011impl Drop for TemporaryRustOutput {
1012 fn drop(&mut self) {
1013 let _ = fs::remove_dir_all(&self.directory);
1014 }
1015}
1016
1017#[derive(Clone, Debug, Deserialize, Serialize)]
1018struct SemanticProgram {
1019 crate_name: String,
1020 functions: Vec<SemanticFunction>,
1021}
1022
1023#[derive(Debug, Deserialize, Serialize)]
1024struct CachedRustAnalysis {
1025 schema: u32,
1026 fingerprint: String,
1027 analysis: FileAnalysis,
1028}
1029
1030#[derive(Clone, Debug, Deserialize, Serialize)]
1031struct SemanticFunction {
1032 base_key: String,
1033 key: String,
1034 display: String,
1035 definition_name: String,
1036 body_span: SourceSpan,
1037 calls: Vec<SemanticCall>,
1038 constructor_spans: Vec<SourceSpan>,
1039 constructor_names: Vec<String>,
1040 parameter_types: Vec<String>,
1041}
1042
1043#[derive(Clone, Debug, Deserialize, Serialize)]
1044struct SemanticCall {
1045 target: SemanticCallTarget,
1046 definition_name: String,
1047 span: SourceSpan,
1048 argument_types: Vec<String>,
1049 argument_flows: Vec<SemanticDynFlow>,
1050}
1051
1052#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1053struct SemanticDynFlow {
1054 concrete_types: Vec<String>,
1055 parameters: Vec<usize>,
1056 unresolved_reasons: BTreeSet<UnresolvedReason>,
1057}
1058
1059#[derive(Clone, Debug, Deserialize, Serialize)]
1060enum SemanticCallTarget {
1061 Direct {
1062 key: String,
1063 display: String,
1064 },
1065 Dynamic {
1066 key: String,
1067 display: String,
1068 candidates: Vec<SemanticDispatchCandidate>,
1069 open: bool,
1070 unresolved_reasons: BTreeSet<UnresolvedReason>,
1071 receiver_flow: SemanticDynFlow,
1072 },
1073 Indirect {
1074 signature: String,
1075 },
1076 Unresolved,
1077}
1078
1079#[derive(Clone, Debug, Deserialize, Serialize)]
1080struct SemanticDispatchCandidate {
1081 key: String,
1082 display: String,
1083}
1084
1085#[derive(Clone, Debug, Default, Eq, PartialEq)]
1086struct DynFlow {
1087 concrete: Vec<TraitRef>,
1088 parameters: Vec<usize>,
1089 unresolved_reasons: BTreeSet<UnresolvedReason>,
1090}
1091
1092impl DynFlow {
1093 fn parameter(index: usize) -> Self {
1094 Self {
1095 concrete: Vec::new(),
1096 parameters: vec![index],
1097 unresolved_reasons: BTreeSet::new(),
1098 }
1099 }
1100
1101 fn concrete(trait_ref: TraitRef) -> Self {
1102 Self {
1103 concrete: vec![trait_ref],
1104 parameters: Vec::new(),
1105 unresolved_reasons: BTreeSet::new(),
1106 }
1107 }
1108
1109 fn unresolved(reason: UnresolvedReason) -> Self {
1110 Self {
1111 concrete: Vec::new(),
1112 parameters: Vec::new(),
1113 unresolved_reasons: [reason].into_iter().collect(),
1114 }
1115 }
1116
1117 fn is_empty(&self) -> bool {
1118 self.concrete.is_empty() && self.parameters.is_empty() && self.unresolved_reasons.is_empty()
1119 }
1120
1121 fn merge(&mut self, other: &Self) -> bool {
1122 let mut changed = false;
1123 for candidate in &other.concrete {
1124 if !self.concrete.contains(candidate) {
1125 self.concrete.push(candidate.clone());
1126 changed = true;
1127 }
1128 }
1129 for parameter in &other.parameters {
1130 if !self.parameters.contains(parameter) {
1131 self.parameters.push(*parameter);
1132 changed = true;
1133 }
1134 }
1135 for reason in &other.unresolved_reasons {
1136 changed |= self.unresolved_reasons.insert(reason.clone());
1137 }
1138 self.parameters.sort_unstable();
1139 changed
1140 }
1141}
1142
1143#[derive(Clone, Debug, Default)]
1144struct ResolvedDynFlow {
1145 concrete: Vec<TraitRef>,
1146 unresolved_reasons: BTreeSet<UnresolvedReason>,
1147}
1148
1149impl ResolvedDynFlow {
1150 fn merge(&mut self, other: &Self) {
1151 for candidate in &other.concrete {
1152 if !self.concrete.contains(candidate) {
1153 self.concrete.push(candidate.clone());
1154 }
1155 }
1156 self.unresolved_reasons
1157 .extend(other.unresolved_reasons.iter().cloned());
1158 }
1159}
1160
1161#[derive(Clone)]
1162struct RawSemanticFunction {
1163 key: String,
1164 display: String,
1165 definition_name: String,
1166 body_span: SourceSpan,
1167 calls: Vec<RawSemanticCall>,
1168 constructor_spans: Vec<SourceSpan>,
1169 constructor_names: Vec<String>,
1170 parameter_types: Vec<String>,
1171}
1172
1173#[derive(Clone)]
1174struct RawSemanticCall {
1175 target: RawSemanticCallTarget,
1176 definition_name: String,
1177 span: SourceSpan,
1178 argument_types: Vec<String>,
1179 argument_flows: Vec<DynFlow>,
1180}
1181
1182#[derive(Clone)]
1183enum RawSemanticCallTarget {
1184 Direct {
1185 key: String,
1186 display: String,
1187 },
1188 Dynamic {
1189 dispatch: Instance,
1190 key: String,
1191 display: String,
1192 receiver: DynFlow,
1193 },
1194 Indirect {
1195 signature: String,
1196 },
1197 Unresolved,
1198}
1199
1200#[derive(Clone)]
1201struct InstanceBody {
1202 instance: Instance,
1203 body: Body,
1204}
1205
1206#[derive(Clone, Default)]
1207struct MirCallFlow {
1208 arguments: Vec<DynFlow>,
1209}
1210
1211#[derive(Clone, Default)]
1212struct BodyDynAnalysis {
1213 calls: Vec<Option<MirCallFlow>>,
1214 return_flow: DynFlow,
1215 return_aliases: Vec<ReturnAlias>,
1216 return_alias_open: bool,
1217 parameter_effects: Vec<DynFlow>,
1218 generated_coroutine_flows: HashMap<Instance, DynFlow>,
1219}
1220
1221#[derive(Clone, Debug, Default)]
1222struct DynSummary {
1223 return_flow: DynFlow,
1224 return_aliases: Vec<ReturnAlias>,
1225 return_alias_open: bool,
1226 parameter_effects: Vec<DynFlow>,
1227}
1228
1229impl DynSummary {
1230 fn merge_analysis(&mut self, analysis: &BodyDynAnalysis) -> bool {
1231 let mut changed = self.return_flow.merge(&analysis.return_flow);
1232 for alias in &analysis.return_aliases {
1233 if !self.return_aliases.contains(alias) {
1234 self.return_aliases.push(alias.clone());
1235 changed = true;
1236 }
1237 }
1238 if analysis.return_alias_open && !self.return_alias_open {
1239 self.return_alias_open = true;
1240 changed = true;
1241 }
1242 if self.parameter_effects.len() < analysis.parameter_effects.len() {
1243 self.parameter_effects
1244 .resize_with(analysis.parameter_effects.len(), DynFlow::default);
1245 }
1246 for (destination, source) in self
1247 .parameter_effects
1248 .iter_mut()
1249 .zip(&analysis.parameter_effects)
1250 {
1251 changed |= destination.merge(source);
1252 }
1253 changed
1254 }
1255}
1256
1257#[derive(Clone, Debug, Eq, PartialEq)]
1258struct ReturnAlias {
1259 parameter: usize,
1260 projection: Vec<ProjectionElem>,
1261}
1262
1263fn collect_rustc_program(path: &Path) -> FrontendResult<SemanticProgram> {
1264 let _driver_guard = RUSTC_DRIVER_LOCK
1265 .lock()
1266 .map_err(|_| std::io::Error::other("rustc semantic driver lock was poisoned"))?;
1267 let output = TemporaryRustOutput::create()?;
1268 let arguments = vec![
1269 "rustc".to_owned(),
1270 path.display().to_string(),
1271 "--crate-name=diffkit_input".to_owned(),
1272 "--crate-type=lib".to_owned(),
1273 "--edition=2024".to_owned(),
1274 "--cap-lints=allow".to_owned(),
1275 format!("--out-dir={}", output.directory.display()),
1276 ];
1277
1278 match rustc_public::run_with_tcx!(&arguments, |tcx| {
1279 if tcx.dcx().has_errors().is_some() {
1280 std::ops::ControlFlow::<SemanticProgram, bool>::Continue(false)
1281 } else {
1282 let (program, expanding_instantiation) = collect_instances();
1283 if expanding_instantiation {
1284 std::ops::ControlFlow::Continue(true)
1285 } else {
1286 std::ops::ControlFlow::Break(program)
1287 }
1288 }
1289 }) {
1290 Err(CompilerError::Interrupted(program)) => Ok(program),
1291 Err(CompilerError::Failed) => Err(std::io::Error::other(format!(
1292 "rustc failed to compile {} for semantic analysis",
1293 path.display()
1294 ))
1295 .into()),
1296 Err(CompilerError::Skipped) => Err(std::io::Error::other(
1297 "rustc skipped semantic analysis before its callback ran",
1298 )
1299 .into()),
1300 Ok(true) => Err(std::io::Error::other(
1301 "rustc semantic analysis stopped at a recursively expanding generic instantiation",
1302 )
1303 .into()),
1304 Ok(false) => Err(std::io::Error::other(
1305 "rustc semantic callback unexpectedly completed without analysis",
1306 )
1307 .into()),
1308 }
1309}
1310
1311fn collect_instances() -> (SemanticProgram, bool) {
1312 let crate_name = rustc_public::local_crate().name.to_string();
1313 let mut dispatch_resolver = DispatchResolver::default();
1314 let (instance_bodies, observed_vtables, generated_coroutines, expanding_instantiation) =
1315 collect_instance_bodies(&mut dispatch_resolver);
1316 let local_instances = instance_bodies
1317 .iter()
1318 .map(|item| item.instance)
1319 .collect::<HashSet<_>>();
1320 let mut summaries = HashMap::<Instance, DynSummary>::new();
1321 let mut body_analyses = HashMap::<Instance, BodyDynAnalysis>::new();
1322
1323 let max_rounds = instance_bodies
1328 .iter()
1329 .map(|item| item.body.locals().len())
1330 .sum::<usize>()
1331 .saturating_add(instance_bodies.len())
1332 .max(1);
1333 let instance_indices = instance_bodies
1334 .iter()
1335 .enumerate()
1336 .map(|(index, item)| (item.instance, index))
1337 .collect::<HashMap<_, _>>();
1338 let mut dependents = vec![Vec::<usize>::new(); instance_bodies.len()];
1339 for (caller, item) in instance_bodies.iter().enumerate() {
1340 for block in &item.body.blocks {
1341 let TerminatorKind::Call { func, .. } = &block.terminator.kind else {
1342 continue;
1343 };
1344 if let Some(callee) = resolve_called_instance(&item.body, func)
1345 && let Some(callee) = instance_indices.get(&callee)
1346 {
1347 dependents[*callee].push(caller);
1348 }
1349 }
1350 }
1351 for callers in &mut dependents {
1352 callers.sort_unstable();
1353 callers.dedup();
1354 }
1355
1356 let mut pending = (0..instance_bodies.len()).collect::<VecDeque<_>>();
1357 let mut queued = vec![true; instance_bodies.len()];
1358 let max_steps = max_rounds.saturating_mul(instance_bodies.len().max(1));
1359 let mut steps = 0usize;
1360 while !pending.is_empty() && steps < max_steps {
1361 let index = pending
1362 .pop_front()
1363 .expect("a non-empty summary worklist has a front item");
1364 queued[index] = false;
1365 steps += 1;
1366 let item = &instance_bodies[index];
1367 let analysis = analyze_body_dyn_flows(&item.body, &summaries, &local_instances);
1368 let changed = summaries
1369 .entry(item.instance)
1370 .or_default()
1371 .merge_analysis(&analysis);
1372 body_analyses.insert(item.instance, analysis);
1373 if changed {
1374 for dependent in &dependents[index] {
1375 if !queued[*dependent] {
1376 pending.push_back(*dependent);
1377 queued[*dependent] = true;
1378 }
1379 }
1380 }
1381 }
1382 let summaries_converged = pending.is_empty();
1383 if !summaries_converged {
1384 for item in &instance_bodies {
1385 if type_contains_dyn(item.body.ret_local().ty) {
1386 summaries
1387 .entry(item.instance)
1388 .or_default()
1389 .return_flow
1390 .unresolved_reasons
1391 .insert(UnresolvedReason::AnalysisLimit);
1392 }
1393 }
1394 body_analyses = instance_bodies
1395 .iter()
1396 .map(|item| {
1397 (
1398 item.instance,
1399 analyze_body_dyn_flows(&item.body, &summaries, &local_instances),
1400 )
1401 })
1402 .collect();
1403 }
1404 let mut raw_functions = Vec::new();
1405 let folded_coroutines = generated_coroutines
1406 .values()
1407 .flatten()
1408 .copied()
1409 .collect::<HashSet<_>>();
1410 let bodies_by_instance = instance_bodies
1411 .iter()
1412 .map(|item| (item.instance, item))
1413 .collect::<HashMap<_, _>>();
1414 for item in &instance_bodies {
1415 if folded_coroutines.contains(&item.instance) {
1416 continue;
1417 }
1418 let instance = item.instance;
1419 let body = &item.body;
1420 let instance_name = stable_instance_name(instance);
1421 let mut calls = Vec::new();
1422 let logical_bodies = source_logical_instance_bodies(
1423 instance,
1424 &generated_coroutines,
1425 &bodies_by_instance,
1426 &body_analyses,
1427 );
1428 for logical_item in &logical_bodies {
1429 let logical_body = &logical_item.item.body;
1430 for (block_index, block) in logical_body.blocks.iter().enumerate() {
1431 let TerminatorKind::Call { func, args, .. } = &block.terminator.kind else {
1432 continue;
1433 };
1434 let resolved_target = resolve_called_instance(logical_body, func);
1435 let definition_name = resolved_target
1436 .map(|target| target.def.name())
1437 .unwrap_or_default();
1438 let mut call_flow = logical_item
1439 .analysis
1440 .calls
1441 .get(block_index)
1442 .and_then(Option::as_ref)
1443 .cloned()
1444 .unwrap_or_default();
1445 for flow in &mut call_flow.arguments {
1446 *flow = resolve_symbolic_flow(flow, &logical_item.context);
1447 }
1448 let target = match resolved_target {
1449 Some(target) => {
1450 let name = stable_instance_name(target);
1451 match target.kind {
1452 InstanceKind::Virtual { .. } => RawSemanticCallTarget::Dynamic {
1453 dispatch: target,
1454 key: normalize_instance_key(&name),
1455 display: normalize_instance_display(&name, &crate_name),
1456 receiver: call_flow.arguments.first().cloned().unwrap_or_else(
1457 || DynFlow::unresolved(UnresolvedReason::AnalysisLimit),
1458 ),
1459 },
1460 _ => RawSemanticCallTarget::Direct {
1461 key: normalize_instance_key(&name),
1462 display: normalize_instance_display(&name, &crate_name),
1463 },
1464 }
1465 }
1466 None => func
1467 .ty(logical_body.locals())
1468 .ok()
1469 .filter(|ty| matches!(ty.kind(), TyKind::RigidTy(RigidTy::FnPtr(_))))
1470 .map(|ty| RawSemanticCallTarget::Indirect {
1471 signature: normalize_type_display(&ty.to_string(), &crate_name),
1472 })
1473 .unwrap_or(RawSemanticCallTarget::Unresolved),
1474 };
1475 calls.push(RawSemanticCall {
1476 target,
1477 definition_name,
1478 span: rustc_source_span(block.terminator.span),
1479 argument_types: args
1480 .iter()
1481 .filter_map(|argument| argument.ty(logical_body.locals()).ok())
1482 .map(|ty| normalize_type_display(&ty.to_string(), &crate_name))
1483 .collect(),
1484 argument_flows: call_flow.arguments,
1485 });
1486 }
1487 }
1488 calls.sort_by_key(|call| {
1489 (
1490 call.span.start_line,
1491 call.span.start_column,
1492 call.span.end_line,
1493 call.span.end_column,
1494 )
1495 });
1496 let mut constructor_spans = logical_bodies
1497 .iter()
1498 .flat_map(|item| &item.item.body.blocks)
1499 .flat_map(|block| &block.statements)
1500 .filter_map(|statement| {
1501 let StatementKind::Assign(_, value) = &statement.kind else {
1502 return None;
1503 };
1504 matches!(value, Rvalue::Aggregate(AggregateKind::Adt(..), _))
1505 .then(|| rustc_source_span(statement.span))
1506 })
1507 .collect::<Vec<_>>();
1508 constructor_spans.sort_by_key(|span| {
1509 (
1510 span.start_line,
1511 span.start_column,
1512 span.end_line,
1513 span.end_column,
1514 )
1515 });
1516 constructor_spans.dedup();
1517 let mut constructor_names = HashSet::new();
1518 let mut visited_constructor_types = HashSet::new();
1519 for logical_item in logical_bodies {
1520 for local in logical_item.item.body.locals() {
1521 collect_adt_constructor_names(
1522 local.ty,
1523 &mut constructor_names,
1524 &mut visited_constructor_types,
1525 );
1526 }
1527 }
1528 let mut constructor_names = constructor_names.into_iter().collect::<Vec<_>>();
1529 constructor_names.sort();
1530 raw_functions.push(RawSemanticFunction {
1531 key: normalize_instance_key(&instance_name),
1532 display: normalize_instance_display(&instance_name, &crate_name),
1533 definition_name: instance.def.name().to_string(),
1534 body_span: rustc_source_span(body.span),
1535 calls,
1536 constructor_spans,
1537 constructor_names,
1538 parameter_types: body
1539 .arg_locals()
1540 .iter()
1541 .map(|argument| normalize_type_display(&argument.ty.to_string(), &crate_name))
1542 .collect(),
1543 });
1544 }
1545
1546 let mut functions = specialize_semantic_functions(
1547 &raw_functions,
1548 &observed_vtables,
1549 &mut dispatch_resolver,
1550 &crate_name,
1551 );
1552 functions.sort_by(|left, right| left.key.cmp(&right.key));
1553 (
1554 SemanticProgram {
1555 crate_name,
1556 functions,
1557 },
1558 expanding_instantiation,
1559 )
1560}
1561
1562fn collect_instance_bodies(
1563 dispatch_resolver: &mut DispatchResolver,
1564) -> (
1565 Vec<InstanceBody>,
1566 Vec<TraitRef>,
1567 HashMap<Instance, Vec<Instance>>,
1568 bool,
1569) {
1570 let mut queue = rustc_public::all_local_items()
1571 .into_iter()
1572 .filter(|item| matches!(item.kind(), rustc_public::ItemKind::Fn))
1573 .filter_map(|item| Instance::try_from(item).ok())
1574 .map(|instance| (instance, HashMap::new()))
1575 .collect::<VecDeque<_>>();
1576 let mut visited = HashSet::new();
1577 let mut bodies = Vec::new();
1578 let mut observed_vtables = Vec::<TraitRef>::new();
1579 let mut observed_vtable_keys = HashSet::new();
1580 let mut virtual_dispatches = Vec::<Instance>::new();
1581 let mut observed_dispatches = HashSet::<Instance>::new();
1582 let mut dispatch_pairs = VecDeque::<(Instance, usize)>::new();
1583 let mut generated_coroutines = HashMap::<Instance, Vec<Instance>>::new();
1584 let mut expanding_instantiation = false;
1585
1586 while !queue.is_empty() || !dispatch_pairs.is_empty() {
1587 while let Some((instance, history)) = queue.pop_front() {
1588 if !visited.insert(instance) {
1589 continue;
1590 }
1591 let Some(body) = instance.body() else {
1592 continue;
1593 };
1594
1595 let previous_vtable_count = observed_vtables.len();
1596 collect_observed_vtables(&body, &mut observed_vtables, &mut observed_vtable_keys);
1597 for vtable_index in previous_vtable_count..observed_vtables.len() {
1598 dispatch_pairs.extend(
1599 virtual_dispatches
1600 .iter()
1601 .map(|dispatch| (*dispatch, vtable_index)),
1602 );
1603 }
1604
1605 for block in &body.blocks {
1606 for statement in &block.statements {
1607 let StatementKind::Assign(_, Rvalue::Aggregate(kind, _)) = &statement.kind
1608 else {
1609 continue;
1610 };
1611 let AggregateKind::Coroutine(definition, arguments) = kind else {
1612 continue;
1613 };
1614 if let Ok(generated) = Instance::resolve(FnDef(definition.def_id()), arguments)
1615 && generated.has_body()
1616 {
1617 enqueue_rust_instance(
1618 &mut queue,
1619 generated,
1620 &history,
1621 &visited,
1622 &mut expanding_instantiation,
1623 );
1624 let children = generated_coroutines.entry(instance).or_default();
1625 if !children.contains(&generated) {
1626 children.push(generated);
1627 }
1628 }
1629 }
1630 let TerminatorKind::Call {
1631 func,
1632 args,
1633 destination,
1634 ..
1635 } = &block.terminator.kind
1636 else {
1637 continue;
1638 };
1639 let Some(target) = resolve_called_instance(&body, func) else {
1640 continue;
1641 };
1642 if matches!(target.kind, InstanceKind::Virtual { .. }) {
1643 if observed_dispatches.insert(target) {
1644 virtual_dispatches.push(target);
1645 dispatch_pairs
1646 .extend((0..observed_vtables.len()).map(|index| (target, index)));
1647 }
1648 } else if target.has_body()
1649 && (target.def.krate().is_local
1650 || instance_body_is_in_analysis_root(target)
1651 || call_boundary_contains_dyn(&body, args, destination))
1652 {
1653 enqueue_rust_instance(
1654 &mut queue,
1655 target,
1656 &history,
1657 &visited,
1658 &mut expanding_instantiation,
1659 );
1660 }
1661 }
1662 bodies.push(InstanceBody { instance, body });
1663 }
1664
1665 while let Some((dispatch, vtable_index)) = dispatch_pairs.pop_front() {
1666 let Some(candidate) =
1667 dispatch_resolver.observed_candidate(dispatch, &observed_vtables[vtable_index])
1668 else {
1669 continue;
1670 };
1671 if candidate.has_body() && !visited.contains(&candidate) {
1672 enqueue_rust_instance(
1673 &mut queue,
1674 candidate,
1675 &HashMap::new(),
1676 &visited,
1677 &mut expanding_instantiation,
1678 );
1679 }
1680 }
1681 }
1682 (
1683 bodies,
1684 observed_vtables,
1685 generated_coroutines,
1686 expanding_instantiation,
1687 )
1688}
1689
1690const MAX_GROWING_INSTANCE_CHAIN: usize = 8;
1691const MAX_NAMED_INSTANCE_RECURSION: usize = 8;
1692const MAX_GENERATED_INSTANCE_RECURSION: usize = 64;
1693
1694type InstanceExpansionHistory = HashMap<rustc_public::DefId, (usize, usize, usize)>;
1695
1696fn enqueue_rust_instance(
1697 queue: &mut VecDeque<(Instance, InstanceExpansionHistory)>,
1698 instance: Instance,
1699 history: &InstanceExpansionHistory,
1700 visited: &HashSet<Instance>,
1701 expanding_instantiation: &mut bool,
1702) {
1703 if visited.contains(&instance) {
1704 return;
1705 }
1706 let definition = instance.def.def_id();
1707 let size = instance.name().as_str().len();
1708 let (growth, recursion) =
1709 history
1710 .get(&definition)
1711 .map_or((0, 0), |(previous, growth, recursion)| {
1712 (
1713 usize::from(size > *previous).saturating_add(*growth),
1714 recursion.saturating_add(1),
1715 )
1716 });
1717 let definition_name = instance.def.name();
1718 let definition_name = definition_name.as_str();
1719 let recursion_limit = if definition_name.contains("{closure")
1720 || definition_name.contains("{async")
1721 || definition_name.contains("{coroutine")
1722 {
1723 MAX_GENERATED_INSTANCE_RECURSION
1724 } else {
1725 MAX_NAMED_INSTANCE_RECURSION
1726 };
1727 if growth >= MAX_GROWING_INSTANCE_CHAIN || recursion >= recursion_limit {
1728 *expanding_instantiation = true;
1729 return;
1730 }
1731 let mut next_history = history.clone();
1732 next_history.insert(definition, (size, growth, recursion));
1733 queue.push_back((instance, next_history));
1734}
1735
1736struct SourceLogicalInstanceBody<'a> {
1737 item: &'a InstanceBody,
1738 analysis: BodyDynAnalysis,
1739 context: Vec<DynFlow>,
1740}
1741
1742fn source_logical_instance_bodies<'a>(
1743 root: Instance,
1744 generated_coroutines: &HashMap<Instance, Vec<Instance>>,
1745 bodies: &HashMap<Instance, &'a InstanceBody>,
1746 analyses: &HashMap<Instance, BodyDynAnalysis>,
1747) -> Vec<SourceLogicalInstanceBody<'a>> {
1748 let Some(root_body) = bodies.get(&root) else {
1749 return Vec::new();
1750 };
1751 let root_context: Vec<DynFlow> = root_body
1752 .body
1753 .arg_locals()
1754 .iter()
1755 .enumerate()
1756 .map(|(index, argument)| {
1757 if type_contains_dyn(argument.ty) || type_may_hide_dyn_provenance(argument.ty) {
1758 DynFlow::parameter(index)
1759 } else {
1760 DynFlow::default()
1761 }
1762 })
1763 .collect();
1764 let mut pending: VecDeque<(Instance, Vec<DynFlow>)> = VecDeque::from([(root, root_context)]);
1765 let mut visited = HashSet::new();
1766 let mut result = Vec::new();
1767 while let Some((instance, context)) = pending.pop_front() {
1768 if !visited.insert(instance) {
1769 continue;
1770 }
1771 let Some(item) = bodies.get(&instance).copied() else {
1772 continue;
1773 };
1774 let analysis = analyses.get(&instance).cloned().unwrap_or_default();
1775 if let Some(children) = generated_coroutines.get(&instance) {
1776 for child in children {
1777 let Some(child_body) = bodies.get(child) else {
1778 continue;
1779 };
1780 let captured = analysis
1781 .generated_coroutine_flows
1782 .get(child)
1783 .cloned()
1784 .unwrap_or_else(|| DynFlow::unresolved(UnresolvedReason::AnalysisLimit));
1785 let child_context: Vec<DynFlow> = child_body
1786 .body
1787 .arg_locals()
1788 .iter()
1789 .enumerate()
1790 .map(|(index, argument)| {
1791 if !type_contains_dyn(argument.ty)
1792 && !type_may_hide_dyn_provenance(argument.ty)
1793 {
1794 DynFlow::default()
1795 } else if index == 0 {
1796 resolve_symbolic_flow(&captured, &context)
1797 } else {
1798 DynFlow::unresolved(UnresolvedReason::AnalysisLimit)
1799 }
1800 })
1801 .collect();
1802 pending.push_back((*child, child_context));
1803 }
1804 }
1805 result.push(SourceLogicalInstanceBody {
1806 item,
1807 analysis,
1808 context,
1809 });
1810 }
1811 result
1812}
1813
1814fn instance_body_is_in_analysis_root(instance: Instance) -> bool {
1815 let Some(root) = env::var_os(RUSTC_ANALYSIS_ROOT).map(PathBuf::from) else {
1816 return false;
1817 };
1818 let Some(body) = instance.body() else {
1819 return false;
1820 };
1821 let file = rustc_source_span(body.span).file;
1822 file.starts_with(&root)
1823 || file
1824 .canonicalize()
1825 .ok()
1826 .zip(root.canonicalize().ok())
1827 .is_some_and(|(file, root)| file.starts_with(root))
1828}
1829
1830fn call_boundary_contains_dyn(body: &Body, arguments: &[Operand], destination: &Place) -> bool {
1831 arguments
1832 .iter()
1833 .filter_map(|argument| argument.ty(body.locals()).ok())
1834 .any(type_contains_dyn)
1835 || destination.ty(body.locals()).is_ok_and(type_contains_dyn)
1836}
1837
1838fn collect_observed_vtables(
1839 body: &rustc_public::mir::Body,
1840 observed: &mut Vec<TraitRef>,
1841 seen: &mut HashSet<(rustc_public::ty::TraitDef, GenericArgs)>,
1842) {
1843 for block in &body.blocks {
1844 for statement in &block.statements {
1845 let StatementKind::Assign(
1846 _,
1847 Rvalue::Cast(
1848 CastKind::PointerCoercion(PointerCoercion::Unsize),
1849 operand,
1850 target_ty,
1851 ),
1852 ) = &statement.kind
1853 else {
1854 continue;
1855 };
1856 let Ok(source_ty) = operand.ty(body.locals()) else {
1857 continue;
1858 };
1859 let Some((concrete_ty, principal)) = dyn_coercion(source_ty, *target_ty) else {
1860 continue;
1861 };
1862 if !principal.bound_vars.is_empty() {
1866 continue;
1867 }
1868 let principal = principal.value;
1869 let trait_ref = TraitRef::new(principal.def_id, concrete_ty, &principal.generic_args);
1870 if seen.insert((trait_ref.def_id, trait_ref.args().clone())) {
1871 observed.push(trait_ref);
1872 }
1873 }
1874 }
1875}
1876
1877fn dyn_coercion(source: Ty, target: Ty) -> Option<(Ty, Binder<ExistentialTraitRef>)> {
1878 let target_kind = target.kind();
1879 if let Some(principal) = target_kind.trait_principal() {
1880 if let TyKind::RigidTy(RigidTy::Ref(_, inner, _) | RigidTy::RawPtr(inner, _)) =
1881 source.kind()
1882 {
1883 return dyn_coercion(inner, target);
1884 }
1885 return (!type_contains_dyn(source)).then_some((source, principal));
1889 }
1890
1891 match (source.kind(), target_kind) {
1892 (
1893 TyKind::RigidTy(RigidTy::Ref(_, source, _)),
1894 TyKind::RigidTy(RigidTy::Ref(_, target, _)),
1895 )
1896 | (
1897 TyKind::RigidTy(RigidTy::RawPtr(source, _)),
1898 TyKind::RigidTy(RigidTy::RawPtr(target, _)),
1899 ) => dyn_coercion(source, target),
1900 (
1901 TyKind::RigidTy(RigidTy::Adt(source_def, source_args)),
1902 TyKind::RigidTy(RigidTy::Adt(target_def, target_args)),
1903 ) if source_def == target_def => source_args
1904 .0
1905 .iter()
1906 .zip(&target_args.0)
1907 .filter_map(|(source, target)| Some((*source.ty()?, *target.ty()?)))
1908 .find_map(|(source, target)| dyn_coercion(source, target)),
1909 _ => None,
1910 }
1911}
1912
1913fn resolve_called_instance(body: &Body, operand: &Operand) -> Option<Instance> {
1914 let function_type = operand.ty(body.locals()).ok()?;
1915 let function_kind = function_type.kind();
1916 let (definition, arguments) = function_kind.fn_def()?;
1917 Instance::resolve(definition, arguments).ok()
1918}
1919
1920fn resolve_dispatch_candidate(dispatch: Instance, trait_ref: &TraitRef) -> Option<Instance> {
1921 let InstanceKind::Virtual { idx } = dispatch.kind else {
1922 return None;
1923 };
1924 if type_contains_dyn(trait_ref.self_ty()) {
1925 return None;
1926 }
1927 let dispatch_arguments = dispatch.args();
1928 let receiver = dispatch_arguments.0.first()?.ty()?;
1929 let principal = dynamic_principal(*receiver)?;
1930 if !principal.bound_vars.is_empty() {
1931 return None;
1932 }
1933 let principal = principal.value;
1934 let dispatch_ref = TraitRef::new(
1935 principal.def_id,
1936 trait_ref.self_ty(),
1937 &principal.generic_args,
1938 );
1939 let VtblEntry::Method(candidate) = dispatch_ref.vtable_entry(idx)? else {
1940 return None;
1941 };
1942 Some(candidate)
1943}
1944
1945fn dispatch_principal_matches_observed_vtable(dispatch: Instance, observed: &TraitRef) -> bool {
1946 dispatch
1947 .args()
1948 .0
1949 .first()
1950 .and_then(GenericArgKind::ty)
1951 .and_then(|receiver| dynamic_principal(*receiver))
1952 .is_some_and(|principal| {
1953 principal.bound_vars.is_empty() && principal.value.def_id == observed.def_id
1954 })
1955}
1956
1957fn resolve_observed_dispatch_candidate(
1958 dispatch: Instance,
1959 observed: &TraitRef,
1960 trait_method_implementations: &mut HashMap<rustc_public::DefId, Option<rustc_public::DefId>>,
1961) -> Option<Instance> {
1962 if dispatch_principal_matches_observed_vtable(dispatch, observed) {
1963 return resolve_dispatch_candidate(dispatch, observed);
1964 }
1965 let trait_method = dispatch.def.def_id();
1966 observed.vtable_entries().into_iter().find_map(|entry| {
1967 let VtblEntry::Method(candidate) = entry else {
1968 return None;
1969 };
1970 let candidate_method = candidate.def.def_id();
1971 (candidate_method == trait_method
1972 || trait_method_for_impl_method(candidate_method, trait_method_implementations)
1973 == Some(trait_method))
1974 .then_some(candidate)
1975 })
1976}
1977
1978fn trait_method_for_impl_method(
1979 implementation_method: rustc_public::DefId,
1980 cache: &mut HashMap<rustc_public::DefId, Option<rustc_public::DefId>>,
1981) -> Option<rustc_public::DefId> {
1982 if let Some(trait_method) = cache.get(&implementation_method) {
1983 return *trait_method;
1984 }
1985 let Some(parent) = implementation_method.parent() else {
1986 cache.insert(implementation_method, None);
1987 return None;
1988 };
1989 for item in ImplDef(parent).associated_items() {
1990 let trait_method = match item.container {
1991 AssocContainer::TraitImpl(trait_item) => Some(trait_item.def_id()),
1992 AssocContainer::InherentImpl | AssocContainer::Trait => None,
1993 };
1994 cache.insert(item.def_id.def_id(), trait_method);
1995 }
1996 *cache.entry(implementation_method).or_insert(None)
1997}
1998
1999type DispatchCandidateCache = HashMap<
2000 (
2001 Instance,
2002 rustc_public::ty::TraitDef,
2003 rustc_public::ty::GenericArgs,
2004 ),
2005 Option<Instance>,
2006>;
2007
2008#[derive(Default)]
2009struct DispatchResolver {
2010 observed: DispatchCandidateCache,
2011 concrete: DispatchCandidateCache,
2012 trait_methods: HashMap<rustc_public::DefId, Option<rustc_public::DefId>>,
2013}
2014
2015impl DispatchResolver {
2016 fn observed_candidate(&mut self, dispatch: Instance, observed: &TraitRef) -> Option<Instance> {
2017 let key = (dispatch, observed.def_id, observed.args().clone());
2018 if let Some(candidate) = self.observed.get(&key) {
2019 return *candidate;
2020 }
2021 let candidate =
2022 resolve_observed_dispatch_candidate(dispatch, observed, &mut self.trait_methods);
2023 self.observed.insert(key, candidate);
2024 candidate
2025 }
2026
2027 fn concrete_candidate(&mut self, dispatch: Instance, observed: &TraitRef) -> Option<Instance> {
2028 let key = (dispatch, observed.def_id, observed.args().clone());
2029 if let Some(candidate) = self.concrete.get(&key) {
2030 return *candidate;
2031 }
2032 let candidate = resolve_dispatch_candidate(dispatch, observed);
2033 self.concrete.insert(key, candidate);
2034 candidate
2035 }
2036}
2037
2038fn dynamic_principal(ty: Ty) -> Option<Binder<ExistentialTraitRef>> {
2039 let kind = ty.kind();
2040 if let Some(principal) = kind.trait_principal() {
2041 return Some(principal);
2042 }
2043 match kind {
2044 TyKind::RigidTy(RigidTy::Ref(_, inner, _) | RigidTy::RawPtr(inner, _)) => {
2045 dynamic_principal(inner)
2046 }
2047 TyKind::RigidTy(RigidTy::Adt(_, arguments)) => arguments
2048 .0
2049 .iter()
2050 .filter_map(GenericArgKind::ty)
2051 .find_map(|ty| dynamic_principal(*ty)),
2052 _ => None,
2053 }
2054}
2055
2056#[derive(Clone, Default)]
2057struct DynState {
2058 flows: HashMap<Place, DynFlow>,
2059 aliases: HashMap<Place, Vec<Place>>,
2060 uncertain_aliases: HashSet<Place>,
2064 constant_indices: HashMap<usize, u64>,
2065 known: HashSet<Place>,
2069}
2070
2071fn analyze_body_dyn_flows(
2072 body: &Body,
2073 summaries: &HashMap<Instance, DynSummary>,
2074 analyzed_instances: &HashSet<Instance>,
2075) -> BodyDynAnalysis {
2076 let mut result = BodyDynAnalysis {
2077 calls: vec![None; body.blocks.len()],
2078 return_flow: DynFlow::default(),
2079 return_aliases: Vec::new(),
2080 return_alias_open: false,
2081 parameter_effects: vec![DynFlow::default(); body.arg_locals().len()],
2082 generated_coroutine_flows: HashMap::new(),
2083 };
2084 if body.blocks.is_empty() {
2085 return result;
2086 }
2087
2088 let mut entry = DynState::default();
2089 for (index, argument) in body.arg_locals().iter().enumerate() {
2090 if type_contains_dyn(argument.ty) || type_may_hide_dyn_provenance(argument.ty) {
2091 entry
2092 .flows
2093 .insert(Place::from(index + 1), DynFlow::parameter(index));
2094 }
2095 }
2096 let mut incoming = vec![None::<DynState>; body.blocks.len()];
2097 incoming[0] = Some(entry);
2098 let mut pending = VecDeque::from([0usize]);
2099
2100 let mut iterations = 0usize;
2101 let max_iterations = body
2102 .blocks
2103 .len()
2104 .saturating_mul(body.locals().len().saturating_add(1))
2105 .saturating_mul(16)
2106 .max(1);
2107 while let Some(block_index) = pending.pop_front() {
2108 iterations += 1;
2109 if iterations > max_iterations {
2110 degrade_dyn_analysis_to_unknown(body, &mut result);
2111 break;
2112 }
2113 let Some(mut state) = incoming[block_index].clone() else {
2114 continue;
2115 };
2116 let block = &body.blocks[block_index];
2117 for statement in &block.statements {
2118 transfer_dyn_statement(
2119 body,
2120 &mut state,
2121 statement,
2122 &mut result.parameter_effects,
2123 &mut result.generated_coroutine_flows,
2124 );
2125 }
2126
2127 match &block.terminator.kind {
2128 TerminatorKind::Call {
2129 func,
2130 args,
2131 destination,
2132 target,
2133 ..
2134 } => {
2135 let resolved = resolve_called_instance(body, func);
2136 let argument_flows = args
2137 .iter()
2138 .map(|argument| dyn_flow_for_operand(body, &state, argument))
2139 .collect::<Vec<_>>();
2140 merge_call_flow(
2141 &mut result.calls[block_index],
2142 &MirCallFlow {
2143 arguments: argument_flows.clone(),
2144 },
2145 );
2146
2147 let destination_flow = resolved.map_or_else(
2148 || unknown_if_dyn_place(body, destination, UnresolvedReason::ExternalCode),
2149 |callee| {
2150 if matches!(callee.kind, InstanceKind::Virtual { .. }) {
2151 unknown_if_dyn_place(body, destination, UnresolvedReason::ExternalCode)
2152 } else if let Some(summary) = summaries.get(&callee) {
2153 resolve_symbolic_flow(&summary.return_flow, &argument_flows)
2154 } else if analyzed_instances.contains(&callee) {
2155 DynFlow::default()
2156 } else {
2157 external_return_flow(body, args, destination, &argument_flows)
2158 }
2159 },
2160 );
2161
2162 apply_call_memory_effects(
2163 &mut state,
2164 CallMemoryInputs {
2165 body,
2166 arguments: args,
2167 resolved,
2168 argument_flows: &argument_flows,
2169 summaries,
2170 analyzed_instances,
2171 },
2172 &mut result.parameter_effects,
2173 );
2174
2175 let returned_alias = returned_call_alias(
2176 body,
2177 &state,
2178 args,
2179 destination,
2180 resolved,
2181 summaries,
2182 analyzed_instances,
2183 );
2184
2185 for successor in block.terminator.successors() {
2186 let mut outgoing = state.clone();
2187 if Some(successor) == *target {
2188 if destination.projection.is_empty() {
2189 outgoing.constant_indices.remove(&destination.local);
2190 }
2191 write_dyn_place(&mut outgoing, destination, destination_flow.clone());
2192 if let Some(alias) = &returned_alias {
2193 outgoing
2194 .aliases
2195 .insert(destination.clone(), alias.targets.clone());
2196 if alias.uncertain {
2197 outgoing.uncertain_aliases.insert(destination.clone());
2198 } else {
2199 outgoing.uncertain_aliases.remove(destination);
2200 }
2201 }
2202 }
2203 if merge_dyn_state(&mut incoming[successor], &outgoing) {
2204 pending.push_back(successor);
2205 }
2206 }
2207 }
2208 TerminatorKind::Return => {
2209 let return_place = Place::from(0);
2210 let flow = read_dyn_place(&state, &return_place);
2211 result.return_flow.merge(&flow);
2212 let targets = resolve_alias_places(&state, &return_place);
2213 if place_alias_is_uncertain(&state, &return_place) {
2214 result.return_alias_open = true;
2215 }
2216 for target in targets {
2217 if target.local == 0 {
2218 let rendered_return = body.ret_local().ty.to_string();
2219 result.return_alias_open |= rendered_return.starts_with('&')
2220 || rendered_return.starts_with("*const ")
2221 || rendered_return.starts_with("*mut ");
2222 continue;
2223 }
2224 if target.local > body.arg_locals().len() {
2225 result.return_alias_open = true;
2226 continue;
2227 }
2228 let mut projection = target.projection.clone();
2229 if projection
2230 .first()
2231 .is_some_and(|element| matches!(element, ProjectionElem::Deref))
2232 {
2233 projection.remove(0);
2234 }
2235 let alias = ReturnAlias {
2236 parameter: target.local - 1,
2237 projection,
2238 };
2239 if !result.return_aliases.contains(&alias) {
2240 result.return_aliases.push(alias);
2241 }
2242 }
2243 }
2244 _ => {
2245 for successor in block.terminator.successors() {
2246 if merge_dyn_state(&mut incoming[successor], &state) {
2247 pending.push_back(successor);
2248 }
2249 }
2250 }
2251 }
2252 }
2253 result
2254}
2255
2256fn external_return_flow(
2257 body: &Body,
2258 arguments: &[Operand],
2259 destination: &Place,
2260 argument_flows: &[DynFlow],
2261) -> DynFlow {
2262 if !destination.ty(body.locals()).is_ok_and(type_contains_dyn) {
2263 return DynFlow::default();
2264 }
2265 let mut flow = DynFlow::unresolved(UnresolvedReason::ExternalCode);
2266 for (argument, argument_flow) in arguments.iter().zip(argument_flows) {
2267 if argument.ty(body.locals()).is_ok_and(type_contains_dyn) {
2268 flow.merge(argument_flow);
2269 }
2270 }
2271 flow
2272}
2273
2274struct ReturnedCallAlias {
2275 targets: Vec<Place>,
2276 uncertain: bool,
2277}
2278
2279#[allow(clippy::too_many_arguments)]
2280fn returned_call_alias(
2281 body: &Body,
2282 state: &DynState,
2283 arguments: &[Operand],
2284 destination: &Place,
2285 resolved: Option<Instance>,
2286 summaries: &HashMap<Instance, DynSummary>,
2287 analyzed_instances: &HashSet<Instance>,
2288) -> Option<ReturnedCallAlias> {
2289 let destination_ty = destination.ty(body.locals()).ok()?;
2290 let rendered_destination = destination_ty.to_string();
2291 let direct_reference = rendered_destination.starts_with('&')
2292 || rendered_destination.starts_with("*const ")
2293 || rendered_destination.starts_with("*mut ");
2294 let borrowed_container = type_may_borrow_dyn(destination_ty);
2295 if !direct_reference && !borrowed_container {
2296 return None;
2297 }
2298
2299 let summary = resolved.and_then(|callee| summaries.get(&callee));
2300 let (sources, mut uncertain) = if let Some(summary) = summary {
2301 let sources = if direct_reference {
2302 let mut sources = summary.return_aliases.clone();
2303 for parameter in &summary.return_flow.parameters {
2304 if !sources.iter().any(|source| source.parameter == *parameter) {
2305 sources.push(ReturnAlias {
2306 parameter: *parameter,
2307 projection: Vec::new(),
2308 });
2309 }
2310 }
2311 sources
2312 } else if borrowed_container {
2313 summary
2314 .return_flow
2315 .parameters
2316 .iter()
2317 .map(|parameter| ReturnAlias {
2318 parameter: *parameter,
2319 projection: Vec::new(),
2320 })
2321 .collect()
2322 } else {
2323 Vec::new()
2324 };
2325 if sources.is_empty() {
2326 return None;
2327 }
2328 (
2329 sources,
2330 !direct_reference
2331 || (summary.return_alias_open
2332 && (summary.return_flow.parameters.is_empty()
2333 || !summary.return_flow.concrete.is_empty()
2334 || !summary.return_flow.unresolved_reasons.is_empty())),
2335 )
2336 } else {
2337 if resolved.is_some_and(|callee| analyzed_instances.contains(&callee)) {
2338 return None;
2339 }
2340 (
2341 arguments
2342 .iter()
2343 .enumerate()
2344 .filter_map(|(index, argument)| {
2345 argument
2346 .ty(body.locals())
2347 .is_ok_and(|ty| type_contains_dyn(ty) || type_may_hide_dyn_provenance(ty))
2348 .then_some(ReturnAlias {
2349 parameter: index,
2350 projection: Vec::new(),
2351 })
2352 })
2353 .collect(),
2354 true,
2355 )
2356 };
2357
2358 let mut targets = Vec::new();
2359 for source in sources {
2360 let Some(place) = arguments.get(source.parameter).and_then(operand_place) else {
2361 continue;
2362 };
2363 uncertain |= place_alias_is_uncertain(state, place);
2364 for mut target in resolve_alias_places(state, place) {
2365 target.projection.extend(source.projection.iter().cloned());
2366 if !targets.contains(&target) {
2367 targets.push(target);
2368 }
2369 }
2370 }
2371 (!targets.is_empty()).then_some(ReturnedCallAlias { targets, uncertain })
2372}
2373
2374fn type_may_borrow_dyn(ty: Ty) -> bool {
2375 if !type_contains_dyn(ty) {
2376 return false;
2377 }
2378 let rendered = ty.to_string();
2379 rendered.starts_with('&')
2380 || rendered.starts_with("*const ")
2381 || rendered.starts_with("*mut ")
2382 || rendered.contains("<'_,")
2383 || rendered.contains("<'_, ")
2384}
2385
2386struct CallMemoryInputs<'a> {
2391 body: &'a Body,
2392 arguments: &'a [Operand],
2393 resolved: Option<Instance>,
2394 argument_flows: &'a [DynFlow],
2395 summaries: &'a HashMap<Instance, DynSummary>,
2396 analyzed_instances: &'a HashSet<Instance>,
2397}
2398
2399fn apply_call_memory_effects(
2400 state: &mut DynState,
2401 inputs: CallMemoryInputs<'_>,
2402 caller_effects: &mut [DynFlow],
2403) {
2404 let CallMemoryInputs {
2405 body,
2406 arguments,
2407 resolved,
2408 argument_flows,
2409 summaries,
2410 analyzed_instances,
2411 } = inputs;
2412 if let Some(summary) = resolved.and_then(|callee| summaries.get(&callee)) {
2413 for (index, effect) in summary.parameter_effects.iter().enumerate() {
2414 if effect.is_empty() {
2415 continue;
2416 }
2417 let resolved_effect = resolve_symbolic_flow(effect, argument_flows);
2418 let Some(argument) = arguments.get(index) else {
2419 continue;
2420 };
2421 if let Some(place) = operand_place(argument) {
2422 merge_dyn_place(state, place, &resolved_effect);
2423 }
2424 propagate_region_effect(argument_flows.get(index), &resolved_effect, caller_effects);
2425 }
2426 return;
2427 }
2428 if resolved.is_some_and(|callee| analyzed_instances.contains(&callee)) {
2429 return;
2433 }
2434
2435 let mut stored = DynFlow::default();
2436 for (argument, flow) in arguments.iter().zip(argument_flows) {
2437 let Ok(ty) = argument.ty(body.locals()) else {
2438 continue;
2439 };
2440 if !is_mutable_reference(ty) && type_contains_dyn(ty) {
2441 stored.merge(flow);
2442 }
2443 }
2444 if stored.is_empty() {
2445 return;
2446 }
2447 stored
2448 .unresolved_reasons
2449 .insert(UnresolvedReason::ExternalCode);
2450 for (index, argument) in arguments.iter().enumerate() {
2451 let Ok(ty) = argument.ty(body.locals()) else {
2452 continue;
2453 };
2454 if !is_mutable_reference(ty) || !type_contains_dyn(ty) {
2455 continue;
2456 }
2457 let Some(place) = operand_place(argument) else {
2458 continue;
2459 };
2460 merge_dyn_place(state, place, &stored);
2461 propagate_region_effect(argument_flows.get(index), &stored, caller_effects);
2462 }
2463}
2464
2465fn propagate_region_effect(
2466 region: Option<&DynFlow>,
2467 effect: &DynFlow,
2468 caller_effects: &mut [DynFlow],
2469) {
2470 let Some(region) = region else {
2471 return;
2472 };
2473 for parameter in ®ion.parameters {
2474 if let Some(destination) = caller_effects.get_mut(*parameter) {
2475 destination.merge(effect);
2476 }
2477 }
2478}
2479
2480fn operand_place(operand: &Operand) -> Option<&Place> {
2481 match operand {
2482 Operand::Copy(place) | Operand::Move(place) => Some(place),
2483 Operand::Constant(_) | Operand::RuntimeChecks(_) => None,
2484 }
2485}
2486
2487fn is_mutable_reference(ty: Ty) -> bool {
2488 matches!(
2489 ty.kind(),
2490 TyKind::RigidTy(RigidTy::Ref(_, _, rustc_public::mir::Mutability::Mut))
2491 | TyKind::RigidTy(RigidTy::RawPtr(_, rustc_public::mir::Mutability::Mut))
2492 )
2493}
2494
2495fn degrade_dyn_analysis_to_unknown(body: &Body, analysis: &mut BodyDynAnalysis) {
2496 if type_contains_dyn(body.ret_local().ty) {
2497 analysis
2498 .return_flow
2499 .unresolved_reasons
2500 .insert(UnresolvedReason::AnalysisLimit);
2501 }
2502 for (block_index, block) in body.blocks.iter().enumerate() {
2503 let TerminatorKind::Call { args, .. } = &block.terminator.kind else {
2504 continue;
2505 };
2506 let call = analysis.calls[block_index].get_or_insert_with(|| MirCallFlow {
2507 arguments: vec![DynFlow::default(); args.len()],
2508 });
2509 call.arguments.resize_with(args.len(), DynFlow::default);
2510 for (argument, flow) in args.iter().zip(&mut call.arguments) {
2511 if argument.ty(body.locals()).is_ok_and(type_contains_dyn) {
2512 flow.unresolved_reasons
2513 .insert(UnresolvedReason::AnalysisLimit);
2514 }
2515 }
2516 }
2517}
2518
2519fn transfer_dyn_statement(
2520 body: &Body,
2521 state: &mut DynState,
2522 statement: &rustc_public::mir::Statement,
2523 parameter_effects: &mut [DynFlow],
2524 generated_coroutine_flows: &mut HashMap<Instance, DynFlow>,
2525) {
2526 let StatementKind::Assign(destination, value) = &statement.kind else {
2527 return;
2528 };
2529 if destination.projection.is_empty() {
2530 let constant = constant_index_for_rvalue(body, state, value);
2531 match constant {
2532 Some(constant) => {
2533 state.constant_indices.insert(destination.local, constant);
2534 }
2535 None => {
2536 state.constant_indices.remove(&destination.local);
2537 }
2538 }
2539 }
2540 match value {
2541 Rvalue::Ref(_, _, source) | Rvalue::AddressOf(_, source) | Rvalue::CopyForDeref(source) => {
2542 let targets = resolve_alias_places(state, source);
2543 state.aliases.insert(destination.clone(), targets);
2544 if place_alias_is_uncertain(state, source) {
2545 state.uncertain_aliases.insert(destination.clone());
2546 } else {
2547 state.uncertain_aliases.remove(destination);
2548 }
2549 }
2550 Rvalue::Use(Operand::Copy(source) | Operand::Move(source), _)
2551 | Rvalue::Cast(_, Operand::Copy(source) | Operand::Move(source), _) => {
2552 if let Some(targets) = state.aliases.get(source).cloned() {
2553 state.aliases.insert(destination.clone(), targets);
2554 if place_alias_is_uncertain(state, source) {
2555 state.uncertain_aliases.insert(destination.clone());
2556 } else {
2557 state.uncertain_aliases.remove(destination);
2558 }
2559 } else {
2560 state.aliases.remove(destination);
2561 state.uncertain_aliases.remove(destination);
2562 }
2563 }
2564 _ => {
2565 state.aliases.remove(destination);
2566 state.uncertain_aliases.remove(destination);
2567 }
2568 }
2569 let flow = dyn_flow_for_rvalue(body, state, value);
2570 if let Rvalue::Aggregate(AggregateKind::Coroutine(definition, arguments), _) = value
2571 && let Ok(coroutine) = Instance::resolve(FnDef(definition.def_id()), arguments)
2572 {
2573 generated_coroutine_flows
2574 .entry(coroutine)
2575 .or_default()
2576 .merge(&flow);
2577 }
2578 if !destination.projection.is_empty() {
2579 let region = read_dyn_place(state, &Place::from(destination.local));
2580 propagate_region_effect(Some(®ion), &flow, parameter_effects);
2581 }
2582 write_dyn_place(state, destination, flow);
2583
2584 let Rvalue::Aggregate(kind, operands) = value else {
2585 return;
2586 };
2587 for (index, operand) in operands.iter().enumerate() {
2588 let Ok(operand_ty) = operand.ty(body.locals()) else {
2589 continue;
2590 };
2591 let projection = match kind {
2592 AggregateKind::Tuple | AggregateKind::Adt(..) => {
2593 Some(ProjectionElem::Field(index, operand_ty))
2594 }
2595 AggregateKind::Array(_) => Some(ProjectionElem::ConstantIndex {
2596 offset: index as u64,
2597 min_length: operands.len() as u64,
2598 from_end: false,
2599 }),
2600 AggregateKind::Closure(..)
2601 | AggregateKind::Coroutine(..)
2602 | AggregateKind::CoroutineClosure(..)
2603 | AggregateKind::RawPtr(..) => None,
2604 };
2605 let Some(projection) = projection else {
2606 continue;
2607 };
2608 let mut field = destination.clone();
2609 if let AggregateKind::Adt(definition, variant, ..) = kind
2610 && definition.kind().is_enum()
2611 {
2612 field.projection.push(ProjectionElem::Downcast(*variant));
2613 }
2614 field.projection.push(projection);
2615 write_dyn_place(state, &field, dyn_flow_for_operand(body, state, operand));
2616 }
2617}
2618
2619fn constant_index_for_rvalue(body: &Body, state: &DynState, value: &Rvalue) -> Option<u64> {
2620 let operand = match value {
2621 Rvalue::Use(operand, _) | Rvalue::Cast(_, operand, _) => operand,
2622 _ => return None,
2623 };
2624 match operand {
2625 Operand::Constant(constant)
2626 if operand.ty(body.locals()).is_ok_and(|ty| {
2627 matches!(ty.kind(), TyKind::RigidTy(RigidTy::Uint(UintTy::Usize)))
2628 }) =>
2629 {
2630 constant.const_.eval_target_usize().ok()
2631 }
2632 Operand::Constant(_) => None,
2633 Operand::Copy(place) | Operand::Move(place) if place.projection.is_empty() => {
2634 state.constant_indices.get(&place.local).copied()
2635 }
2636 Operand::Copy(_) | Operand::Move(_) | Operand::RuntimeChecks(_) => None,
2637 }
2638}
2639
2640fn dyn_flow_for_rvalue(body: &Body, state: &DynState, value: &Rvalue) -> DynFlow {
2641 let mut flow = match value {
2642 Rvalue::Use(operand, _) | Rvalue::Repeat(operand, _) => {
2643 dyn_flow_for_operand(body, state, operand)
2644 }
2645 Rvalue::Cast(CastKind::PointerCoercion(PointerCoercion::Unsize), operand, target_ty) => {
2646 operand
2647 .ty(body.locals())
2648 .ok()
2649 .and_then(|source_ty| dyn_coercion(source_ty, *target_ty))
2650 .and_then(|(concrete_ty, principal)| {
2651 principal.bound_vars.is_empty().then(|| {
2652 let principal = principal.value;
2653 DynFlow::concrete(TraitRef::new(
2654 principal.def_id,
2655 concrete_ty,
2656 &principal.generic_args,
2657 ))
2658 })
2659 })
2660 .unwrap_or_else(|| dyn_flow_for_operand(body, state, operand))
2661 }
2662 Rvalue::Cast(_, operand, _) => dyn_flow_for_operand(body, state, operand),
2663 Rvalue::Ref(_, _, place) | Rvalue::AddressOf(_, place) | Rvalue::CopyForDeref(place) => {
2664 read_dyn_place(state, place)
2665 }
2666 Rvalue::Aggregate(_, operands) => {
2667 let mut aggregate = DynFlow::default();
2668 for operand in operands {
2669 aggregate.merge(&dyn_flow_for_operand(body, state, operand));
2670 }
2671 aggregate
2672 }
2673 Rvalue::BinaryOp(_, left, right) | Rvalue::CheckedBinaryOp(_, left, right) => {
2674 let mut combined = dyn_flow_for_operand(body, state, left);
2675 combined.merge(&dyn_flow_for_operand(body, state, right));
2676 combined
2677 }
2678 Rvalue::UnaryOp(_, operand) => dyn_flow_for_operand(body, state, operand),
2679 Rvalue::ThreadLocalRef(_) | Rvalue::Discriminant(_) | Rvalue::Len(_) => DynFlow::default(),
2680 };
2681 if flow.is_empty()
2682 && !rvalue_provenance_is_known(body, state, value)
2683 && value.ty(body.locals()).is_ok_and(type_contains_dyn)
2684 {
2685 flow.unresolved_reasons
2686 .insert(UnresolvedReason::AnalysisLimit);
2687 }
2688 flow
2689}
2690
2691fn dyn_flow_for_operand(body: &Body, state: &DynState, operand: &Operand) -> DynFlow {
2692 let mut flow = match operand {
2693 Operand::Copy(place) | Operand::Move(place) => read_dyn_place(state, place),
2694 Operand::Constant(_) | Operand::RuntimeChecks(_) => DynFlow::default(),
2695 };
2696 let known = operand_provenance_is_known(body, state, operand);
2697 if flow.is_empty() && !known && operand.ty(body.locals()).is_ok_and(type_contains_dyn) {
2698 flow.unresolved_reasons
2699 .insert(UnresolvedReason::ExternalMemory);
2700 }
2701 flow
2702}
2703
2704fn operand_provenance_is_known(body: &Body, state: &DynState, operand: &Operand) -> bool {
2705 if operand
2706 .ty(body.locals())
2707 .is_ok_and(|ty| !type_contains_dyn(ty))
2708 {
2709 return true;
2710 }
2711 match operand {
2712 Operand::Copy(place) | Operand::Move(place) => dyn_place_is_known(state, place),
2713 Operand::Constant(constant) => match constant.const_.kind() {
2714 ConstantKind::ZeroSized => true,
2715 ConstantKind::Allocated(allocation) => allocation.provenance.ptrs.is_empty(),
2716 ConstantKind::Ty(_) | ConstantKind::Unevaluated(_) | ConstantKind::Param(_) => false,
2717 },
2718 Operand::RuntimeChecks(_) => true,
2719 }
2720}
2721
2722fn rvalue_provenance_is_known(body: &Body, state: &DynState, value: &Rvalue) -> bool {
2723 match value {
2724 Rvalue::Use(operand, _)
2725 | Rvalue::Repeat(operand, _)
2726 | Rvalue::Cast(_, operand, _)
2727 | Rvalue::UnaryOp(_, operand) => operand_provenance_is_known(body, state, operand),
2728 Rvalue::Ref(_, _, place) | Rvalue::AddressOf(_, place) | Rvalue::CopyForDeref(place) => {
2729 dyn_place_is_known(state, place)
2730 }
2731 Rvalue::Aggregate(_, operands) => operands
2732 .iter()
2733 .all(|operand| operand_provenance_is_known(body, state, operand)),
2734 Rvalue::BinaryOp(_, left, right) | Rvalue::CheckedBinaryOp(_, left, right) => {
2735 operand_provenance_is_known(body, state, left)
2736 && operand_provenance_is_known(body, state, right)
2737 }
2738 Rvalue::ThreadLocalRef(_) | Rvalue::Discriminant(_) | Rvalue::Len(_) => true,
2739 }
2740}
2741
2742fn unknown_if_dyn_place(body: &Body, place: &Place, reason: UnresolvedReason) -> DynFlow {
2743 place
2744 .ty(body.locals())
2745 .ok()
2746 .filter(|ty| type_contains_dyn(*ty))
2747 .map_or_else(DynFlow::default, |_| DynFlow::unresolved(reason))
2748}
2749
2750fn type_contains_dyn(ty: Ty) -> bool {
2751 ty.to_string().contains("dyn ")
2755}
2756
2757fn type_may_hide_dyn_provenance(ty: Ty) -> bool {
2758 let rendered = ty.to_string();
2759 [
2760 "{closure@",
2761 "{async closure@",
2762 "{async closure body@",
2763 "{async block@",
2764 "{coroutine@",
2765 "{generator@",
2766 ]
2767 .iter()
2768 .any(|marker| rendered.contains(marker))
2769}
2770
2771fn collect_adt_constructor_names(ty: Ty, names: &mut HashSet<String>, visited: &mut HashSet<Ty>) {
2772 if !visited.insert(ty) {
2773 return;
2774 }
2775 if ty.to_string().contains("{async closure@") {
2781 return;
2782 }
2783 match ty.kind() {
2784 TyKind::RigidTy(RigidTy::Adt(definition, arguments)) => {
2785 names.extend(
2786 definition
2787 .variants()
2788 .into_iter()
2789 .map(|variant| variant.name().to_string()),
2790 );
2791 for argument in arguments.0 {
2792 if let Some(ty) = argument.ty() {
2793 collect_adt_constructor_names(*ty, names, visited);
2794 }
2795 }
2796 }
2797 TyKind::RigidTy(
2802 RigidTy::FnDef(..)
2803 | RigidTy::Closure(..)
2804 | RigidTy::Coroutine(..)
2805 | RigidTy::CoroutineClosure(..)
2806 | RigidTy::CoroutineWitness(..),
2807 ) => {}
2808 TyKind::RigidTy(
2809 RigidTy::Array(ty, _)
2810 | RigidTy::Slice(ty)
2811 | RigidTy::RawPtr(ty, _)
2812 | RigidTy::Ref(_, ty, _),
2813 ) => collect_adt_constructor_names(ty, names, visited),
2814 TyKind::RigidTy(RigidTy::Tuple(types)) => {
2815 for ty in types {
2816 collect_adt_constructor_names(ty, names, visited);
2817 }
2818 }
2819 _ => {}
2820 }
2821}
2822
2823fn read_dyn_place(state: &DynState, place: &Place) -> DynFlow {
2824 if let Some(flow) = state.flows.get(place) {
2825 return flow.clone();
2826 }
2827 let alias_targets = resolve_alias_places(state, place);
2828 if alias_targets.len() != 1 || alias_targets.first() != Some(place) {
2829 let mut aliased = DynFlow::default();
2830 for target in alias_targets {
2831 if target != *place {
2832 aliased.merge(&read_dyn_place_without_alias(state, &target));
2833 }
2834 }
2835 if !aliased.is_empty() {
2836 return aliased;
2837 }
2838 }
2839 read_dyn_place_without_alias(state, place)
2840}
2841
2842fn read_dyn_place_without_alias(state: &DynState, place: &Place) -> DynFlow {
2843 if let Some(flow) = state.flows.get(place) {
2844 return flow.clone();
2845 }
2846 let mut indexed = DynFlow::default();
2847 let mut indexed_match = false;
2848 for (candidate, flow) in &state.flows {
2849 if constant_index_place_matches(state, place, candidate) {
2850 indexed_match = true;
2851 indexed.merge(flow);
2852 }
2853 }
2854 if indexed_match {
2855 return indexed;
2856 }
2857 let mut ancestor = place.clone();
2858 while ancestor.projection.pop().is_some() {
2859 if let Some(flow) = state.flows.get(&ancestor) {
2860 return flow.clone();
2865 }
2866 }
2867 let mut result = DynFlow::default();
2868 for (candidate, flow) in &state.flows {
2869 if place_is_prefix(place, candidate) {
2870 result.merge(flow);
2871 }
2872 }
2873 result
2874}
2875
2876fn write_dyn_place(state: &mut DynState, place: &Place, flow: DynFlow) {
2877 let mut targets = Vec::new();
2878 for target in resolve_alias_places(state, place) {
2879 if !targets.contains(&target) {
2880 targets.push(target);
2881 }
2882 }
2883 let initializes_alias = place.projection.is_empty() && state.aliases.contains_key(place);
2884 if !initializes_alias && place_alias_is_uncertain(state, place) {
2885 invalidate_dyn_places(state, &targets, UnresolvedReason::ExternalCode);
2886 return;
2887 }
2888 let may_alias = targets.len() > 1;
2889 for target in targets {
2890 let target = concretize_constant_index_place(state, &target).unwrap_or(target);
2891 if weak_write_through_runtime_index(state, &target, &flow) {
2892 continue;
2893 }
2894 if may_alias {
2895 state.known.insert(target.clone());
2896 if !flow.is_empty() {
2897 state.flows.entry(target).or_default().merge(&flow);
2898 }
2899 continue;
2900 }
2901 state
2902 .flows
2903 .retain(|candidate, _| !place_is_prefix(&target, candidate));
2904 state
2905 .known
2906 .retain(|candidate| !place_is_prefix(&target, candidate));
2907 state.known.insert(target.clone());
2908 if !flow.is_empty() {
2909 state.flows.insert(target, flow.clone());
2910 }
2911 }
2912}
2913
2914fn concretize_constant_index_place(state: &DynState, target: &Place) -> Option<Place> {
2915 let mut candidates = Vec::new();
2916 for candidate in state.flows.keys().chain(&state.known) {
2917 if constant_index_place_matches(state, target, candidate) && !candidates.contains(candidate)
2918 {
2919 candidates.push(candidate.clone());
2920 }
2921 }
2922 (candidates.len() == 1).then(|| candidates.remove(0))
2923}
2924
2925fn constant_index_place_matches(state: &DynState, query: &Place, candidate: &Place) -> bool {
2926 if query.local != candidate.local || query.projection.len() != candidate.projection.len() {
2927 return false;
2928 }
2929 let mut used_constant_index = false;
2930 let matches =
2931 query
2932 .projection
2933 .iter()
2934 .zip(&candidate.projection)
2935 .all(|(query, candidate)| match (query, candidate) {
2936 (
2937 ProjectionElem::Index(local),
2938 ProjectionElem::ConstantIndex {
2939 offset,
2940 from_end: false,
2941 ..
2942 },
2943 ) => {
2944 used_constant_index = true;
2945 state.constant_indices.get(local) == Some(offset)
2946 }
2947 _ => query == candidate,
2948 });
2949 used_constant_index && matches
2950}
2951
2952fn weak_write_through_runtime_index(state: &mut DynState, target: &Place, flow: &DynFlow) -> bool {
2953 let Some(index) = target
2954 .projection
2955 .iter()
2956 .position(|projection| matches!(projection, ProjectionElem::Index(_)))
2957 else {
2958 return false;
2959 };
2960 let mut aggregate = target.clone();
2961 aggregate.projection.truncate(index);
2962 state.known.insert(aggregate.clone());
2963 if !flow.is_empty() {
2964 state
2965 .flows
2966 .entry(aggregate.clone())
2967 .or_default()
2968 .merge(flow);
2969 }
2970
2971 let overlapping = state
2972 .flows
2973 .keys()
2974 .filter(|candidate| runtime_index_write_overlaps(target, candidate, index))
2975 .cloned()
2976 .collect::<Vec<_>>();
2977 for candidate in overlapping {
2978 state.known.insert(candidate.clone());
2979 if !flow.is_empty() {
2980 state.flows.entry(candidate).or_default().merge(flow);
2981 }
2982 }
2983 true
2984}
2985
2986fn runtime_index_write_overlaps(target: &Place, candidate: &Place, index: usize) -> bool {
2987 if target.local != candidate.local || target.projection.len() != candidate.projection.len() {
2988 return false;
2989 }
2990 target
2991 .projection
2992 .iter()
2993 .zip(&candidate.projection)
2994 .enumerate()
2995 .all(|(projection_index, (target, candidate))| {
2996 if projection_index == index {
2997 matches!(
2998 candidate,
2999 ProjectionElem::Index(_) | ProjectionElem::ConstantIndex { .. }
3000 )
3001 } else {
3002 target == candidate
3003 }
3004 })
3005}
3006
3007fn merge_dyn_place(state: &mut DynState, place: &Place, flow: &DynFlow) {
3008 if flow.is_empty() {
3009 return;
3010 }
3011 let targets = resolve_alias_places(state, place);
3012 if place_alias_is_uncertain(state, place) {
3013 invalidate_dyn_places(state, &targets, UnresolvedReason::ExternalCode);
3014 return;
3015 }
3016 for target in targets {
3017 state.known.insert(target.clone());
3018 state.flows.entry(target).or_default().merge(flow);
3019 }
3020}
3021
3022fn invalidate_dyn_places(state: &mut DynState, places: &[Place], reason: UnresolvedReason) {
3023 for place in places {
3024 state
3025 .flows
3026 .retain(|candidate, _| !place_is_prefix(place, candidate));
3027 state
3028 .known
3029 .retain(|candidate| !place_is_prefix(place, candidate));
3030 state.known.insert(place.clone());
3031 state
3032 .flows
3033 .insert(place.clone(), DynFlow::unresolved(reason.clone()));
3034 }
3035}
3036
3037fn dyn_place_is_known(state: &DynState, place: &Place) -> bool {
3038 let targets = resolve_alias_places(state, place);
3039 !targets.is_empty()
3040 && targets
3041 .iter()
3042 .all(|target| dyn_place_is_known_without_alias(state, target))
3043}
3044
3045fn dyn_place_is_known_without_alias(state: &DynState, place: &Place) -> bool {
3046 if state.known.contains(place) || state.flows.contains_key(place) {
3047 return true;
3048 }
3049 if state
3050 .known
3051 .iter()
3052 .chain(state.flows.keys())
3053 .any(|candidate| constant_index_place_matches(state, place, candidate))
3054 {
3055 return true;
3056 }
3057 let mut ancestor = place.clone();
3058 while ancestor.projection.pop().is_some() {
3059 if state.known.contains(&ancestor) || state.flows.contains_key(&ancestor) {
3060 return true;
3061 }
3062 }
3063 false
3064}
3065
3066fn resolve_alias_places(state: &DynState, place: &Place) -> Vec<Place> {
3067 if let Some(targets) = state.aliases.get(place) {
3068 return targets.clone();
3069 }
3070 let base = Place::from(place.local);
3071 let Some(targets) = state.aliases.get(&base) else {
3072 return vec![place.clone()];
3073 };
3074 let mut suffix = place.projection.as_slice();
3075 if suffix
3076 .first()
3077 .is_some_and(|projection| matches!(projection, ProjectionElem::Deref))
3078 {
3079 suffix = &suffix[1..];
3080 }
3081 targets
3082 .iter()
3083 .cloned()
3084 .map(|mut target| {
3085 target.projection.extend_from_slice(suffix);
3086 target
3087 })
3088 .collect()
3089}
3090
3091fn place_alias_is_uncertain(state: &DynState, place: &Place) -> bool {
3092 if state.uncertain_aliases.contains(place) {
3093 return true;
3094 }
3095 let base = Place::from(place.local);
3096 state.uncertain_aliases.contains(&base)
3097}
3098
3099fn place_is_prefix(prefix: &Place, place: &Place) -> bool {
3100 prefix.local == place.local
3101 && prefix.projection.len() <= place.projection.len()
3102 && place.projection.starts_with(&prefix.projection)
3103}
3104
3105fn merge_dyn_state(destination: &mut Option<DynState>, source: &DynState) -> bool {
3106 let Some(destination) = destination else {
3107 *destination = Some(source.clone());
3108 return true;
3109 };
3110 let mut changed = false;
3111 let constants_before = destination.constant_indices.len();
3112 destination
3113 .constant_indices
3114 .retain(|local, value| source.constant_indices.get(local) == Some(value));
3115 changed |= destination.constant_indices.len() != constants_before;
3116 for (place, flow) in &source.flows {
3117 changed |= destination
3118 .flows
3119 .entry(place.clone())
3120 .or_default()
3121 .merge(flow);
3122 }
3123 for (place, targets) in &source.aliases {
3124 let destination_targets = destination.aliases.entry(place.clone()).or_default();
3125 for target in targets {
3126 if !destination_targets.contains(target) {
3127 destination_targets.push(target.clone());
3128 changed = true;
3129 }
3130 }
3131 }
3132 for place in &source.uncertain_aliases {
3133 changed |= destination.uncertain_aliases.insert(place.clone());
3134 }
3135 for place in &source.known {
3136 changed |= destination.known.insert(place.clone());
3137 }
3138 changed
3139}
3140
3141fn merge_call_flow(destination: &mut Option<MirCallFlow>, source: &MirCallFlow) {
3142 let Some(destination) = destination else {
3143 *destination = Some(source.clone());
3144 return;
3145 };
3146 if destination.arguments.len() < source.arguments.len() {
3147 destination
3148 .arguments
3149 .resize_with(source.arguments.len(), DynFlow::default);
3150 }
3151 for (destination, source) in destination.arguments.iter_mut().zip(&source.arguments) {
3152 destination.merge(source);
3153 }
3154}
3155
3156fn resolve_symbolic_flow(flow: &DynFlow, arguments: &[DynFlow]) -> DynFlow {
3157 let mut resolved = DynFlow {
3158 concrete: flow.concrete.clone(),
3159 parameters: Vec::new(),
3160 unresolved_reasons: flow.unresolved_reasons.clone(),
3161 };
3162 for parameter in &flow.parameters {
3163 if let Some(argument) = arguments.get(*parameter) {
3164 resolved.merge(argument);
3165 } else {
3166 resolved
3167 .unresolved_reasons
3168 .insert(UnresolvedReason::AnalysisLimit);
3169 }
3170 }
3171 resolved
3172}
3173
3174fn specialize_semantic_functions(
3175 raw_functions: &[RawSemanticFunction],
3176 observed_vtables: &[TraitRef],
3177 dispatch_resolver: &mut DispatchResolver,
3178 crate_name: &str,
3179) -> Vec<SemanticFunction> {
3180 let index = raw_functions
3181 .iter()
3182 .enumerate()
3183 .map(|(index, function)| (function.key.clone(), index))
3184 .collect::<HashMap<_, _>>();
3185 let mut incoming = vec![0usize; raw_functions.len()];
3186 for function in raw_functions {
3187 for call in &function.calls {
3188 match &call.target {
3189 RawSemanticCallTarget::Direct { key, .. } => {
3190 if let Some(target) = index.get(key) {
3191 incoming[*target] += 1;
3192 }
3193 }
3194 RawSemanticCallTarget::Dynamic { dispatch, .. } => {
3195 for trait_ref in observed_vtables {
3196 let Some(candidate) =
3197 dispatch_resolver.observed_candidate(*dispatch, trait_ref)
3198 else {
3199 continue;
3200 };
3201 let candidate_name = stable_instance_name(candidate);
3202 if let Some(target) = index.get(&normalize_instance_key(&candidate_name)) {
3203 incoming[*target] += 1;
3204 }
3205 }
3206 }
3207 RawSemanticCallTarget::Indirect { .. } | RawSemanticCallTarget::Unresolved => {}
3208 }
3209 }
3210 }
3211
3212 let mut pending = VecDeque::<(usize, Vec<ResolvedDynFlow>)>::new();
3213 for (function, count) in incoming.iter().enumerate() {
3214 if *count == 0 {
3215 pending.push_back((function, root_dyn_context(&raw_functions[function])));
3216 }
3217 }
3218 let mut visited = HashSet::new();
3219 let mut covered_raw = HashSet::new();
3220 let mut functions = Vec::new();
3221
3222 loop {
3223 while let Some((raw_index, context)) = pending.pop_front() {
3224 let raw = &raw_functions[raw_index];
3225 let function_key = specialized_function_key(raw, &context);
3226 covered_raw.insert(raw_index);
3231 if !visited.insert(function_key.clone()) {
3232 continue;
3233 }
3234 let mut calls = Vec::new();
3235
3236 for raw_call in &raw.calls {
3237 let target = match &raw_call.target {
3238 RawSemanticCallTarget::Direct { key, display } => {
3239 let specialized = index.get(key).map_or_else(
3240 || key.clone(),
3241 |callee_index| {
3242 let callee = &raw_functions[*callee_index];
3243 let callee_context =
3244 call_dyn_context(callee, &raw_call.argument_flows, &context);
3245 let key = specialized_function_key(callee, &callee_context);
3246 pending.push_back((*callee_index, callee_context));
3247 key
3248 },
3249 );
3250 SemanticCallTarget::Direct {
3251 key: specialized,
3252 display: display.clone(),
3253 }
3254 }
3255 RawSemanticCallTarget::Dynamic {
3256 dispatch,
3257 key,
3258 display,
3259 receiver,
3260 } => {
3261 let receiver_flow = semantic_dyn_flow(receiver, crate_name);
3262 let receiver = resolve_context_flow(receiver, &context);
3263 let mut candidates = Vec::new();
3264 for trait_ref in &receiver.concrete {
3265 let Some(candidate) =
3266 dispatch_resolver.concrete_candidate(*dispatch, trait_ref)
3267 else {
3268 continue;
3269 };
3270 let candidate_name = stable_instance_name(candidate);
3271 let raw_candidate_key = normalize_instance_key(&candidate_name);
3272 let candidate_key = index.get(&raw_candidate_key).map_or_else(
3273 || raw_candidate_key.clone(),
3274 |candidate_index| {
3275 let candidate_function = &raw_functions[*candidate_index];
3276 let candidate_context = call_dyn_context(
3277 candidate_function,
3278 &raw_call.argument_flows,
3279 &context,
3280 );
3281 let key = specialized_function_key(
3282 candidate_function,
3283 &candidate_context,
3284 );
3285 pending.push_back((*candidate_index, candidate_context));
3286 key
3287 },
3288 );
3289 if !candidates
3290 .iter()
3291 .any(|existing: &SemanticDispatchCandidate| {
3292 existing.key == candidate_key
3293 })
3294 {
3295 candidates.push(SemanticDispatchCandidate {
3296 key: candidate_key,
3297 display: normalize_instance_display(
3298 &candidate_name,
3299 crate_name,
3300 ),
3301 });
3302 }
3303 }
3304 candidates.sort_by(|left, right| left.key.cmp(&right.key));
3305 SemanticCallTarget::Dynamic {
3306 key: key.clone(),
3307 display: display.clone(),
3308 candidates,
3309 open: !receiver.unresolved_reasons.is_empty(),
3310 unresolved_reasons: receiver.unresolved_reasons,
3311 receiver_flow,
3312 }
3313 }
3314 RawSemanticCallTarget::Indirect { signature } => SemanticCallTarget::Indirect {
3315 signature: signature.clone(),
3316 },
3317 RawSemanticCallTarget::Unresolved => SemanticCallTarget::Unresolved,
3318 };
3319 calls.push(SemanticCall {
3320 target,
3321 definition_name: raw_call.definition_name.clone(),
3322 span: raw_call.span.clone(),
3323 argument_types: raw_call.argument_types.clone(),
3324 argument_flows: raw_call
3325 .argument_flows
3326 .iter()
3327 .map(|flow| semantic_dyn_flow(flow, crate_name))
3328 .collect(),
3329 });
3330 }
3331
3332 functions.push(SemanticFunction {
3333 base_key: raw.key.clone(),
3334 key: function_key,
3335 display: raw.display.clone(),
3336 definition_name: raw.definition_name.clone(),
3337 body_span: raw.body_span.clone(),
3338 calls,
3339 constructor_spans: raw.constructor_spans.clone(),
3340 constructor_names: raw.constructor_names.clone(),
3341 parameter_types: raw.parameter_types.clone(),
3342 });
3343 }
3344
3345 let Some(uncovered) = (0..raw_functions.len()).find(|index| !covered_raw.contains(index))
3346 else {
3347 break;
3348 };
3349 pending.push_back((uncovered, root_dyn_context(&raw_functions[uncovered])));
3350 }
3351 functions
3352}
3353
3354fn root_dyn_context(function: &RawSemanticFunction) -> Vec<ResolvedDynFlow> {
3355 function
3356 .parameter_types
3357 .iter()
3358 .enumerate()
3359 .map(|(index, parameter)| ResolvedDynFlow {
3360 concrete: Vec::new(),
3361 unresolved_reasons: (parameter.contains("dyn ")
3362 || raw_function_uses_dyn_parameter(function, index))
3363 .then_some(UnresolvedReason::OpaqueInput)
3364 .into_iter()
3365 .collect(),
3366 })
3367 .collect()
3368}
3369
3370fn call_dyn_context(
3371 callee: &RawSemanticFunction,
3372 arguments: &[DynFlow],
3373 caller_context: &[ResolvedDynFlow],
3374) -> Vec<ResolvedDynFlow> {
3375 callee
3376 .parameter_types
3377 .iter()
3378 .enumerate()
3379 .map(|(index, parameter)| {
3380 if !parameter.contains("dyn ") && !raw_function_uses_dyn_parameter(callee, index) {
3381 return ResolvedDynFlow::default();
3382 }
3383 arguments.get(index).map_or_else(
3384 || ResolvedDynFlow {
3385 concrete: Vec::new(),
3386 unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
3387 },
3388 |flow| {
3389 let mut resolved = resolve_context_flow(flow, caller_context);
3390 if resolved.concrete.is_empty() && resolved.unresolved_reasons.is_empty() {
3391 resolved
3392 .unresolved_reasons
3393 .insert(UnresolvedReason::AnalysisLimit);
3394 }
3395 resolved
3396 },
3397 )
3398 })
3399 .collect()
3400}
3401
3402fn raw_function_uses_dyn_parameter(function: &RawSemanticFunction, parameter: usize) -> bool {
3403 function.calls.iter().any(|call| {
3404 call.argument_flows
3405 .iter()
3406 .any(|flow| flow.parameters.contains(¶meter))
3407 || matches!(
3408 &call.target,
3409 RawSemanticCallTarget::Dynamic { receiver, .. }
3410 if receiver.parameters.contains(¶meter)
3411 )
3412 })
3413}
3414
3415fn resolve_context_flow(flow: &DynFlow, context: &[ResolvedDynFlow]) -> ResolvedDynFlow {
3416 let mut resolved = ResolvedDynFlow {
3417 concrete: flow.concrete.clone(),
3418 unresolved_reasons: flow.unresolved_reasons.clone(),
3419 };
3420 for parameter in &flow.parameters {
3421 if let Some(argument) = context.get(*parameter) {
3422 resolved.merge(argument);
3423 } else {
3424 resolved
3425 .unresolved_reasons
3426 .insert(UnresolvedReason::AnalysisLimit);
3427 }
3428 }
3429 resolved
3430}
3431
3432fn semantic_dyn_flow(flow: &DynFlow, crate_name: &str) -> SemanticDynFlow {
3433 let mut concrete_types = flow
3434 .concrete
3435 .iter()
3436 .map(|trait_ref| normalize_type_display(&trait_ref.self_ty().to_string(), crate_name))
3437 .collect::<Vec<_>>();
3438 concrete_types.sort();
3439 concrete_types.dedup();
3440 SemanticDynFlow {
3441 concrete_types,
3442 parameters: flow.parameters.clone(),
3443 unresolved_reasons: flow.unresolved_reasons.clone(),
3444 }
3445}
3446
3447fn specialized_function_key(function: &RawSemanticFunction, context: &[ResolvedDynFlow]) -> String {
3448 let bindings = context
3449 .iter()
3450 .enumerate()
3451 .filter(|(index, value)| {
3452 function
3453 .parameter_types
3454 .get(*index)
3455 .is_some_and(|parameter| parameter.contains("dyn "))
3456 || !value.concrete.is_empty()
3457 || !value.unresolved_reasons.is_empty()
3458 })
3459 .map(|(index, value)| {
3460 let mut candidates = value
3461 .concrete
3462 .iter()
3463 .map(|trait_ref| trait_ref.self_ty().to_string())
3464 .collect::<Vec<_>>();
3465 candidates.sort();
3466 for reason in &value.unresolved_reasons {
3467 candidates.push(format!("?{reason}"));
3468 }
3469 format!("{index}={}", candidates.join("|"))
3470 })
3471 .collect::<Vec<_>>();
3472 if bindings.is_empty() {
3473 function.key.clone()
3474 } else {
3475 format!("{}#ctx[dyn:{}]", function.key, bindings.join(","))
3476 }
3477}
3478
3479#[derive(Clone, Debug, Default, Eq, PartialEq)]
3480struct NamedDynFlow {
3481 concrete_types: BTreeSet<String>,
3482 unresolved_reasons: BTreeSet<UnresolvedReason>,
3483}
3484
3485fn respecialize_merged_semantic_program(program: &mut SemanticProgram) {
3486 if program.functions.is_empty() {
3487 return;
3488 }
3489 let mut templates = BTreeMap::<String, SemanticFunction>::new();
3490 for function in &program.functions {
3491 templates
3492 .entry(function.base_key.clone())
3493 .and_modify(|existing| {
3494 if semantic_template_score(function) > semantic_template_score(existing) {
3495 *existing = function.clone();
3496 }
3497 })
3498 .or_insert_with(|| function.clone());
3499 }
3500 let bases = templates.keys().cloned().collect::<HashSet<_>>();
3501 let mut incoming = bases
3502 .iter()
3503 .cloned()
3504 .map(|key| (key, 0usize))
3505 .collect::<HashMap<_, _>>();
3506 for function in templates.values() {
3507 for call in &function.calls {
3508 let SemanticCallTarget::Direct { key, .. } = &call.target else {
3509 continue;
3510 };
3511 if let Some(base) = semantic_target_base(key, &bases) {
3512 *incoming.entry(base).or_default() += 1;
3513 }
3514 }
3515 }
3516
3517 let mut pending = incoming
3518 .iter()
3519 .filter(|(_, count)| **count == 0)
3520 .filter_map(|(base, _)| {
3521 templates
3522 .get(base)
3523 .map(|function| (base.clone(), root_named_context(function)))
3524 })
3525 .collect::<VecDeque<_>>();
3526 let mut visited = HashSet::new();
3527 let mut covered = HashSet::new();
3528 let mut specialized = Vec::new();
3529
3530 loop {
3531 while let Some((base, context)) = pending.pop_front() {
3532 let Some(template) = templates.get(&base) else {
3533 continue;
3534 };
3535 covered.insert(base.clone());
3536 let key = named_specialized_key(template, &context);
3537 if !visited.insert(key.clone()) {
3538 continue;
3539 }
3540 let mut function = template.clone();
3541 function.key = key;
3542 for call in &mut function.calls {
3543 match &mut call.target {
3544 SemanticCallTarget::Direct { key, .. } => {
3545 let Some(target_base) = semantic_target_base(key, &bases) else {
3546 continue;
3547 };
3548 let Some(target) = templates.get(&target_base) else {
3549 continue;
3550 };
3551 let target_context =
3552 named_call_context(target, &call.argument_flows, &context);
3553 *key = named_specialized_key(target, &target_context);
3554 pending.push_back((target_base, target_context));
3555 }
3556 SemanticCallTarget::Dynamic {
3557 key,
3558 candidates,
3559 open,
3560 unresolved_reasons,
3561 receiver_flow,
3562 ..
3563 } => {
3564 let mut receiver = resolve_named_flow(receiver_flow, &context);
3565 if let Some(argument_receiver) = call.argument_flows.first() {
3566 merge_named_flow(
3567 &mut receiver,
3568 &resolve_named_flow(argument_receiver, &context),
3569 );
3570 }
3571 *candidates = receiver
3572 .concrete_types
3573 .iter()
3574 .filter_map(|concrete| {
3575 let candidate =
3576 named_dispatch_candidate(concrete, key, templates.values())?;
3577 let candidate_context =
3578 named_call_context(candidate, &call.argument_flows, &context);
3579 let candidate_key =
3580 named_specialized_key(candidate, &candidate_context);
3581 pending.push_back((candidate.base_key.clone(), candidate_context));
3582 Some(SemanticDispatchCandidate {
3583 key: candidate_key,
3584 display: candidate.display.clone(),
3585 })
3586 })
3587 .collect();
3588 candidates.sort_by(|left, right| left.key.cmp(&right.key));
3589 candidates.dedup_by(|left, right| left.key == right.key);
3590 *unresolved_reasons = receiver.unresolved_reasons;
3591 *open = !unresolved_reasons.is_empty();
3592 }
3593 SemanticCallTarget::Indirect { .. } | SemanticCallTarget::Unresolved => {}
3594 }
3595 }
3596 specialized.push(function);
3597 }
3598
3599 let Some(uncovered) = templates
3600 .keys()
3601 .find(|base| !covered.contains(*base))
3602 .cloned()
3603 else {
3604 break;
3605 };
3606 let context = root_named_context(&templates[&uncovered]);
3607 pending.push_back((uncovered, context));
3608 }
3609 specialized.sort_by(|left, right| left.key.cmp(&right.key));
3610 program.functions = specialized;
3611}
3612
3613fn merge_named_flow(destination: &mut NamedDynFlow, source: &NamedDynFlow) {
3614 destination
3615 .concrete_types
3616 .extend(source.concrete_types.iter().cloned());
3617 destination
3618 .unresolved_reasons
3619 .extend(source.unresolved_reasons.iter().cloned());
3620}
3621
3622fn semantic_template_score(function: &SemanticFunction) -> usize {
3623 let symbolic = function
3624 .calls
3625 .iter()
3626 .map(|call| {
3627 let arguments = call
3628 .argument_flows
3629 .iter()
3630 .map(|flow| flow.parameters.len())
3631 .sum::<usize>();
3632 let receiver = match &call.target {
3633 SemanticCallTarget::Dynamic { receiver_flow, .. } => receiver_flow.parameters.len(),
3634 _ => 0,
3635 };
3636 arguments + receiver
3637 })
3638 .sum::<usize>();
3639 let exact_base = usize::from(function.key == function.base_key);
3640 symbolic.saturating_mul(10) + exact_base
3641}
3642
3643fn semantic_target_base(key: &str, bases: &HashSet<String>) -> Option<String> {
3644 if bases.contains(key) {
3645 return Some(key.to_owned());
3646 }
3647 bases
3648 .iter()
3649 .filter(|base| key.starts_with(base.as_str()))
3650 .max_by_key(|base| base.len())
3651 .cloned()
3652}
3653
3654fn root_named_context(function: &SemanticFunction) -> Vec<NamedDynFlow> {
3655 function
3656 .parameter_types
3657 .iter()
3658 .enumerate()
3659 .map(|(index, parameter)| NamedDynFlow {
3660 concrete_types: BTreeSet::new(),
3661 unresolved_reasons: (parameter.contains("dyn ")
3662 || semantic_function_uses_dyn_parameter(function, index))
3663 .then_some(UnresolvedReason::OpaqueInput)
3664 .into_iter()
3665 .collect(),
3666 })
3667 .collect()
3668}
3669
3670fn named_call_context(
3671 callee: &SemanticFunction,
3672 arguments: &[SemanticDynFlow],
3673 caller_context: &[NamedDynFlow],
3674) -> Vec<NamedDynFlow> {
3675 callee
3676 .parameter_types
3677 .iter()
3678 .enumerate()
3679 .map(|(index, parameter)| {
3680 if !parameter.contains("dyn ") && !semantic_function_uses_dyn_parameter(callee, index) {
3681 return NamedDynFlow::default();
3682 }
3683 arguments.get(index).map_or_else(
3684 || NamedDynFlow {
3685 concrete_types: BTreeSet::new(),
3686 unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
3687 },
3688 |flow| resolve_named_flow(flow, caller_context),
3689 )
3690 })
3691 .collect()
3692}
3693
3694fn semantic_function_uses_dyn_parameter(function: &SemanticFunction, parameter: usize) -> bool {
3695 function.calls.iter().any(|call| {
3696 call.argument_flows
3697 .iter()
3698 .any(|flow| flow.parameters.contains(¶meter))
3699 || matches!(
3700 &call.target,
3701 SemanticCallTarget::Dynamic { receiver_flow, .. }
3702 if receiver_flow.parameters.contains(¶meter)
3703 )
3704 })
3705}
3706
3707fn resolve_named_flow(flow: &SemanticDynFlow, context: &[NamedDynFlow]) -> NamedDynFlow {
3708 let mut resolved = NamedDynFlow {
3709 concrete_types: flow.concrete_types.iter().cloned().collect(),
3710 unresolved_reasons: flow.unresolved_reasons.clone(),
3711 };
3712 for parameter in &flow.parameters {
3713 if let Some(value) = context.get(*parameter) {
3714 resolved
3715 .concrete_types
3716 .extend(value.concrete_types.iter().cloned());
3717 resolved
3718 .unresolved_reasons
3719 .extend(value.unresolved_reasons.iter().cloned());
3720 } else {
3721 resolved
3722 .unresolved_reasons
3723 .insert(UnresolvedReason::AnalysisLimit);
3724 }
3725 }
3726 resolved
3727}
3728
3729fn named_specialized_key(function: &SemanticFunction, context: &[NamedDynFlow]) -> String {
3730 let bindings = context
3731 .iter()
3732 .enumerate()
3733 .filter(|(index, flow)| {
3734 function
3735 .parameter_types
3736 .get(*index)
3737 .is_some_and(|parameter| parameter.contains("dyn "))
3738 || !flow.concrete_types.is_empty()
3739 || !flow.unresolved_reasons.is_empty()
3740 })
3741 .map(|(index, flow)| {
3742 let mut values = flow.concrete_types.iter().cloned().collect::<Vec<_>>();
3743 values.extend(
3744 flow.unresolved_reasons
3745 .iter()
3746 .map(|reason| format!("?{reason}")),
3747 );
3748 format!("{index}={}", values.join("|"))
3749 })
3750 .collect::<Vec<_>>();
3751 if bindings.is_empty() {
3752 function.base_key.clone()
3753 } else {
3754 format!("{}#ctx[dyn:{}]", function.base_key, bindings.join(","))
3755 }
3756}
3757
3758fn named_dispatch_candidate<'a>(
3759 concrete: &str,
3760 dispatch: &str,
3761 functions: impl Iterator<Item = &'a SemanticFunction>,
3762) -> Option<&'a SemanticFunction> {
3763 let method = dispatch
3764 .rsplit("::")
3765 .next()
3766 .unwrap_or(dispatch)
3767 .trim_end_matches('>');
3768 let concrete = concrete
3769 .rsplit("::")
3770 .next()
3771 .unwrap_or(concrete)
3772 .trim_matches(['&', ' ', '<', '>']);
3773 functions
3774 .filter(|function| {
3775 function.display.ends_with(&format!("{concrete}::{method}"))
3776 || (function.display.ends_with(&format!("::{method}"))
3777 && function.base_key.contains(concrete)
3778 && function.base_key.contains(" as "))
3779 })
3780 .min_by_key(|function| function.base_key.clone())
3781}
3782
3783fn merge_semantic_program(syntax: FileAnalysis, mut semantic: SemanticProgram) -> FileAnalysis {
3784 let mut analysis = FileAnalysis {
3785 source_files: syntax.source_files.clone(),
3786 ..FileAnalysis::default()
3787 };
3788 let file_identities = source_file_identities(&syntax, &semantic);
3789 canonicalize_semantic_closures(&syntax, &mut semantic, &file_identities);
3790 let local_function_keys = semantic
3791 .functions
3792 .iter()
3793 .map(|function| function.key.clone())
3794 .collect::<HashSet<_>>();
3795 let semantic_closure_keys = semantic_closure_keys(&syntax, &semantic, &file_identities);
3796 let mut required_syntax_closures = HashSet::<SymbolId>::new();
3797 let mut matched_syntax_functions = HashSet::<SymbolId>::new();
3798 let generic_syntax_functions = syntax
3799 .facts
3800 .iter()
3801 .filter(|fact| fact.kind == "generic-parameter")
3802 .map(|fact| fact.subject.clone())
3803 .collect::<HashSet<_>>();
3804 let mut syntax_functions_by_file = HashMap::<PathBuf, Vec<usize>>::new();
3805 for (index, function) in syntax.functions.iter().enumerate() {
3806 syntax_functions_by_file
3807 .entry(source_file_identity(&file_identities, &function.span.file).to_path_buf())
3808 .or_default()
3809 .push(index);
3810 }
3811 let mut facts_by_subject = HashMap::<&SymbolId, Vec<&LanguageFact>>::new();
3812 for fact in &syntax.facts {
3813 facts_by_subject
3814 .entry(&fact.subject)
3815 .or_default()
3816 .push(fact);
3817 }
3818 let source_backed_semantic_keys = semantic
3819 .functions
3820 .iter()
3821 .filter(|function| {
3822 let file = source_file_identity(&file_identities, &function.body_span.file);
3823 let Some(candidate_indices) = syntax_functions_by_file.get(file) else {
3824 return false;
3825 };
3826 best_function_template(&syntax.functions, candidate_indices, &function.body_span)
3827 .is_some_and(|template| {
3828 semantic_body_matches_source_template(function, template, &facts_by_subject)
3829 })
3830 })
3831 .map(|function| function.key.clone())
3832 .collect::<HashSet<_>>();
3833 let mut transparent_dispatch_forwarders = semantic
3834 .functions
3835 .iter()
3836 .filter(|function| !source_backed_semantic_keys.contains(&function.key))
3837 .filter_map(|function| {
3838 let [call] = function.calls.as_slice() else {
3839 return None;
3840 };
3841 matches!(call.target, SemanticCallTarget::Dynamic { .. })
3842 .then(|| (function.key.clone(), call.target.clone()))
3843 })
3844 .collect::<HashMap<_, _>>();
3845 loop {
3846 let mut changed = false;
3847 for function in &semantic.functions {
3848 if source_backed_semantic_keys.contains(&function.key)
3849 || transparent_dispatch_forwarders.contains_key(&function.key)
3850 {
3851 continue;
3852 }
3853 let [call] = function.calls.as_slice() else {
3854 continue;
3855 };
3856 let SemanticCallTarget::Direct { key, .. } = &call.target else {
3857 continue;
3858 };
3859 if let Some(target) = transparent_dispatch_forwarders.get(key).cloned() {
3860 transparent_dispatch_forwarders.insert(function.key.clone(), target);
3861 changed = true;
3862 }
3863 }
3864 if !changed {
3865 break;
3866 }
3867 }
3868
3869 for semantic_function in semantic.functions {
3870 let function_file =
3871 source_file_identity(&file_identities, &semantic_function.body_span.file);
3872 let Some(candidate_indices) = syntax_functions_by_file.get(function_file) else {
3873 continue;
3874 };
3875 let Some(template) = best_function_template(
3876 &syntax.functions,
3877 candidate_indices,
3878 &semantic_function.body_span,
3879 ) else {
3880 continue;
3883 };
3884 if !semantic_body_matches_source_template(&semantic_function, template, &facts_by_subject) {
3885 continue;
3886 }
3887 matched_syntax_functions.insert(template.id.clone());
3888 let id = semantic_symbol(semantic_function.key);
3889 let mut claimed_calls = HashSet::new();
3890 let calls = template
3891 .calls
3892 .iter()
3893 .filter_map(|call| {
3894 let Some(match_index) = best_semantic_call(
3895 call,
3896 &semantic_function.calls,
3897 &claimed_calls,
3898 &file_identities,
3899 ) else {
3900 if call.syntax.requires_compiler_confirmation() {
3901 return None;
3902 }
3903 let is_constructor = semantic_function
3904 .constructor_spans
3905 .iter()
3906 .any(|span| call.span.has_same_coordinates(span))
3907 || call_syntax_name(call).is_some_and(|name| {
3908 semantic_function
3909 .constructor_names
3910 .iter()
3911 .any(|constructor| constructor == name)
3912 });
3913 if is_constructor {
3914 return None;
3915 }
3916 let mut unresolved = call.clone();
3917 if let Some(closure) =
3918 source_closure_for_call(template, call, &syntax.functions)
3919 {
3920 unresolved.target = semantic_closure_keys
3921 .get(&closure.id)
3922 .cloned()
3923 .map(semantic_symbol)
3924 .map_or_else(
3925 || {
3926 required_syntax_closures.insert(closure.id.clone());
3927 CallTarget::Direct(closure.id.clone())
3928 },
3929 CallTarget::Direct,
3930 );
3931 unresolved.label =
3932 replace_label_callee(&unresolved.label, closure_label_callee(closure));
3933 }
3934 return Some(unresolved);
3935 };
3936 let mut resolved = call.clone();
3937 claimed_calls.insert(match_index);
3938 let semantic_call = &semantic_function.calls[match_index];
3939 let semantic_target = match &semantic_call.target {
3940 SemanticCallTarget::Direct { key, .. } => transparent_dispatch_forwarders
3941 .get(key)
3942 .unwrap_or(&semantic_call.target),
3943 _ => &semantic_call.target,
3944 };
3945 match semantic_target {
3946 SemanticCallTarget::Direct { key, display } => {
3947 if matches!(
3948 call.syntax.visible(),
3949 CallSyntax::Expression(expression)
3950 if direct_lambda_callee(expression).is_some()
3951 ) && let Some(closure) =
3952 source_closure_for_call(template, call, &syntax.functions)
3953 {
3954 resolved.target = semantic_closure_keys
3955 .get(&closure.id)
3956 .cloned()
3957 .map(semantic_symbol)
3958 .map_or_else(
3959 || {
3960 required_syntax_closures.insert(closure.id.clone());
3961 CallTarget::Direct(closure.id.clone())
3962 },
3963 CallTarget::Direct,
3964 );
3965 resolved.label = replace_label_callee(
3966 &resolved.label,
3967 closure_label_callee(closure),
3968 );
3969 } else {
3970 resolved.target = CallTarget::Direct(semantic_symbol(key.clone()));
3971 if local_function_keys.contains(key) {
3972 resolved.label = replace_label_callee(&resolved.label, display);
3973 }
3974 }
3975 }
3976 SemanticCallTarget::Dynamic {
3977 key,
3978 display,
3979 candidates,
3980 open,
3981 unresolved_reasons,
3982 ..
3983 } => {
3984 resolved.target = CallTarget::Dynamic {
3985 dispatch: semantic_symbol(key.clone()),
3986 candidates: candidates
3987 .iter()
3988 .map(|candidate| DispatchCandidate {
3989 target: semantic_symbol(candidate.key.clone()),
3990 label: replace_dispatch_candidate_label(
3991 &resolved.label,
3992 &candidate.display,
3993 ),
3994 })
3995 .collect(),
3996 resolution: DispatchResolution::from_candidates(
3997 !candidates.is_empty(),
3998 *open,
3999 ),
4000 evidence: DispatchEvidence::ExactFlow,
4001 unresolved_reasons: if unresolved_reasons.is_empty()
4002 && candidates.is_empty()
4003 {
4004 [UnresolvedReason::AnalysisLimit].into_iter().collect()
4005 } else {
4006 unresolved_reasons.clone()
4007 },
4008 };
4009 resolved.label = replace_label_callee(&resolved.label, display);
4010 }
4011 SemanticCallTarget::Indirect { signature } => {
4012 resolved.target = CallTarget::Indirect {
4013 signature: Some(signature.clone()),
4014 reason: UnresolvedReason::FunctionPointer,
4015 };
4016 }
4017 SemanticCallTarget::Unresolved => {
4018 resolved.target = CallTarget::Unresolved;
4019 if let Some(closure) =
4020 source_closure_for_call(template, call, &syntax.functions)
4021 {
4022 resolved.target = semantic_closure_keys
4023 .get(&closure.id)
4024 .cloned()
4025 .map(semantic_symbol)
4026 .map_or_else(
4027 || {
4028 required_syntax_closures.insert(closure.id.clone());
4029 CallTarget::Direct(closure.id.clone())
4030 },
4031 CallTarget::Direct,
4032 );
4033 resolved.label = replace_label_callee(
4034 &resolved.label,
4035 closure_label_callee(closure),
4036 );
4037 }
4038 }
4039 }
4040 let argument_types = semantic_call_argument_types(semantic_call);
4041 resolved.label.typed = Some(annotate_rust_label(
4042 &resolved.label.default,
4043 &argument_types,
4044 ));
4045 Some(resolved)
4046 })
4047 .collect();
4048
4049 let mut function_label = replace_label_callee(&template.label, &semantic_function.display);
4050 function_label.typed = Some(annotate_rust_label(
4051 &function_label.default,
4052 &semantic_function.parameter_types,
4053 ));
4054 analysis.functions.push(FunctionInfo {
4055 id: id.clone(),
4056 label: function_label,
4057 public: template.public,
4058 calls,
4059 span: template.span.clone(),
4060 });
4061 if let Some(facts) = facts_by_subject.get(&template.id) {
4062 analysis.facts.extend(facts.iter().map(|fact| {
4063 let mut fact = (*fact).clone();
4064 fact.subject = id.clone();
4065 fact
4066 }));
4067 }
4068 }
4069
4070 for function in &syntax.functions {
4071 let preserve_closure = required_syntax_closures.contains(&function.id);
4072 let preserve_open_generic = generic_syntax_functions.contains(&function.id)
4073 && !matched_syntax_functions.contains(&function.id);
4074 if !preserve_closure && !preserve_open_generic {
4075 continue;
4076 }
4077 let mut preserved = function.clone();
4078 if preserve_closure {
4079 for call in &mut preserved.calls {
4080 if !matches!(call.target, CallTarget::Unresolved) {
4081 continue;
4082 }
4083 let Some(nested) = source_closure_for_call(function, call, &syntax.functions)
4084 else {
4085 continue;
4086 };
4087 required_syntax_closures.insert(nested.id.clone());
4088 call.target = CallTarget::Direct(nested.id.clone());
4089 call.label = replace_label_callee(&call.label, closure_label_callee(nested));
4090 }
4091 }
4092 analysis.functions.push(preserved);
4093 if preserve_open_generic && let Some(facts) = facts_by_subject.get(&function.id) {
4094 analysis.facts.extend(facts.iter().copied().cloned());
4095 }
4096 }
4097
4098 synthesize_referenced_generic_instances(&mut analysis, &syntax);
4099
4100 analysis
4101}
4102
4103fn semantic_body_matches_source_template(
4104 semantic: &SemanticFunction,
4105 template: &FunctionInfo,
4106 facts_by_subject: &HashMap<&SymbolId, Vec<&LanguageFact>>,
4107) -> bool {
4108 if compiler_closure_marker(&semantic.definition_name).is_none() {
4109 return true;
4110 }
4111 if is_source_closure(template) {
4112 return true;
4113 }
4114 facts_by_subject.get(&template.id).is_some_and(|facts| {
4119 facts
4120 .iter()
4121 .any(|fact| fact.kind == "modifier" && fact.key == "async")
4122 })
4123}
4124
4125fn synthesize_referenced_generic_instances(analysis: &mut FileAnalysis, syntax: &FileAnalysis) {
4126 let generic_parameters = syntax
4127 .facts
4128 .iter()
4129 .filter(|fact| fact.kind == "generic-parameter")
4130 .fold(BTreeMap::<SymbolId, Vec<String>>::new(), |mut map, fact| {
4131 map.entry(fact.subject.clone())
4132 .or_default()
4133 .push(fact.key.clone());
4134 map
4135 });
4136 if generic_parameters.is_empty() {
4137 return;
4138 }
4139
4140 let mut used_templates = HashSet::new();
4141 loop {
4142 let known = analysis
4143 .functions
4144 .iter()
4145 .map(|function| function.id.clone())
4146 .collect::<HashSet<_>>();
4147 let missing = analysis
4148 .functions
4149 .iter()
4150 .flat_map(|function| &function.calls)
4151 .filter_map(|call| match &call.target {
4152 CallTarget::Direct(target)
4153 if !known.contains(target) && target.language.0 == "rust" =>
4154 {
4155 Some(target.clone())
4156 }
4157 _ => None,
4158 })
4159 .collect::<BTreeSet<_>>();
4160 let mut added = Vec::new();
4161 for target in missing {
4162 let Some((base, arguments)) = generic_instance_parts(&target.name) else {
4163 continue;
4164 };
4165 let Some(template) = syntax.functions.iter().find(|function| {
4166 function.id.name == base
4167 && generic_parameters
4168 .get(&function.id)
4169 .is_some_and(|parameters| parameters.len() == arguments.len())
4170 }) else {
4171 continue;
4172 };
4173 let parameters = &generic_parameters[&template.id];
4174 let bindings = parameters
4175 .iter()
4176 .cloned()
4177 .zip(arguments.iter().cloned())
4178 .collect::<HashMap<_, _>>();
4179 let mut function = template.clone();
4180 function.id = target.clone();
4181 let display = normalize_instance_display(&target.name, "");
4182 function.label = replace_label_callee(&function.label, &display);
4183 for call in &mut function.calls {
4184 substitute_generic_call(call, &bindings, &analysis.functions);
4185 }
4186 used_templates.insert(template.id.clone());
4187 added.push(function);
4188 }
4189 if added.is_empty() {
4190 break;
4191 }
4192 analysis.functions.extend(added);
4193 }
4194
4195 let displays = analysis
4196 .functions
4197 .iter()
4198 .map(|function| {
4199 (
4200 function.id.clone(),
4201 outer_call_arguments_start(&function.label.default)
4202 .map_or(function.label.default.clone(), |start| {
4203 function.label.default[..start].to_owned()
4204 }),
4205 )
4206 })
4207 .collect::<HashMap<_, _>>();
4208 for function in &mut analysis.functions {
4209 for call in &mut function.calls {
4210 let CallTarget::Direct(target) = &call.target else {
4211 continue;
4212 };
4213 if let Some(display) = displays.get(target) {
4214 call.label = replace_label_callee(&call.label, display);
4215 }
4216 }
4217 }
4218 analysis
4219 .functions
4220 .retain(|function| !used_templates.contains(&function.id));
4221 analysis
4222 .facts
4223 .retain(|fact| !used_templates.contains(&fact.subject));
4224}
4225
4226fn generic_instance_parts(instance: &str) -> Option<(String, Vec<String>)> {
4227 let start = instance.find('<')?;
4228 let end = instance.rfind('>')?;
4229 if end <= start {
4230 return None;
4231 }
4232 let base = instance[..start]
4233 .rsplit("::")
4234 .next()
4235 .unwrap_or(&instance[..start])
4236 .to_owned();
4237 let arguments = split_rust_arguments(&instance[start + 1..end])
4238 .into_iter()
4239 .map(simplify_type_paths)
4240 .collect();
4241 Some((base, arguments))
4242}
4243
4244fn substitute_generic_call(
4245 call: &mut CallSite,
4246 bindings: &HashMap<String, String>,
4247 functions: &[FunctionInfo],
4248) {
4249 let CallTarget::Direct(target) = &call.target else {
4250 return;
4251 };
4252 let Some(container) = target.container.as_deref() else {
4253 return;
4254 };
4255 let parameter = container
4256 .split_once(" as ")
4257 .map_or(container, |(parameter, _)| parameter);
4258 let Some(concrete) = bindings.get(parameter) else {
4259 return;
4260 };
4261 let callee = format!("{concrete}::{}", target.name);
4262 call.label = replace_label_callee(&call.label, &callee);
4263 call.target = functions
4264 .iter()
4265 .find(|function| {
4266 outer_call_arguments_start(&function.label.default)
4267 .is_some_and(|start| function.label.default[..start] == callee)
4268 })
4269 .map(|function| function.id.clone())
4270 .map_or_else(
4271 || {
4272 CallTarget::Direct(SymbolId {
4273 language: LanguageId::new("rust"),
4274 module: target.module.clone(),
4275 container: Some(concrete.clone()),
4276 name: target.name.clone(),
4277 })
4278 },
4279 CallTarget::Direct,
4280 );
4281}
4282
4283fn semantic_closure_keys(
4284 syntax: &FileAnalysis,
4285 semantic: &SemanticProgram,
4286 file_identities: &HashMap<PathBuf, PathBuf>,
4287) -> HashMap<SymbolId, String> {
4288 syntax
4289 .functions
4290 .iter()
4291 .filter(|function| is_source_closure(function))
4292 .filter_map(|closure| {
4293 semantic
4294 .functions
4295 .iter()
4296 .filter(|function| {
4297 source_file_identity(file_identities, &closure.span.file)
4298 == source_file_identity(file_identities, &function.body_span.file)
4299 && closure.span.contains(&function.body_span)
4300 })
4301 .min_by_key(|function| function.body_span.extent())
4302 .map(|function| (closure.id.clone(), function.key.clone()))
4303 })
4304 .collect()
4305}
4306
4307fn source_closure_for_call<'a>(
4308 owner: &FunctionInfo,
4309 call: &CallSite,
4310 functions: &'a [FunctionInfo],
4311) -> Option<&'a FunctionInfo> {
4312 let expected_container = owner.id.qualified_parts().join("::");
4313 let expected_lambda = match call.syntax.visible() {
4314 CallSyntax::Expression(expression) => direct_lambda_callee(expression),
4315 _ => call_syntax_name(call).map(|name| format!("λ{name}")),
4316 }?;
4317 functions.iter().find(|function| {
4318 is_source_closure(function)
4319 && function.id.container.as_deref() == Some(expected_container.as_str())
4320 && closure_label_callee(function) == expected_lambda
4321 })
4322}
4323
4324fn direct_lambda_callee(expression: &str) -> Option<String> {
4325 let mut expression = expression.trim();
4326 while let Some(inner) = expression.strip_prefix('(') {
4327 let mut depth = 1usize;
4328 let mut closing = None;
4329 for (index, character) in inner.char_indices() {
4330 match character {
4331 '(' => depth += 1,
4332 ')' => {
4333 depth = depth.checked_sub(1)?;
4334 if depth == 0 {
4335 closing = Some(index);
4336 break;
4337 }
4338 }
4339 _ => {}
4340 }
4341 }
4342 let closing = closing?;
4343 if closing + ')'.len_utf8() != inner.len() {
4344 break;
4345 }
4346 expression = inner[..closing].trim();
4347 }
4348 expression.starts_with('λ').then(|| expression.to_owned())
4349}
4350
4351fn closure_label_callee(function: &FunctionInfo) -> &str {
4352 outer_call_arguments_start(&function.label.default)
4353 .map_or(function.label.default.as_str(), |start| {
4354 &function.label.default[..start]
4355 })
4356}
4357
4358#[derive(Clone)]
4359struct SemanticClosureSource {
4360 span: SourceSpan,
4361 identity: String,
4362 lambda: String,
4363}
4364
4365#[derive(Clone, Copy)]
4366enum ClosureRewrite {
4367 Identity,
4368 Display,
4369}
4370
4371fn canonicalize_semantic_closures(
4372 syntax: &FileAnalysis,
4373 semantic: &mut SemanticProgram,
4374 file_identities: &HashMap<PathBuf, PathBuf>,
4375) {
4376 let closures = syntax
4377 .functions
4378 .iter()
4379 .filter(|function| is_source_closure(function))
4380 .map(|function| SemanticClosureSource {
4381 span: function.span.clone(),
4382 identity: format!("{{lambda-id:{}}}", function.id),
4383 lambda: outer_call_arguments_start(&function.label.default).map_or_else(
4384 || function.label.default.clone(),
4385 |start| function.label.default[..start].to_owned(),
4386 ),
4387 })
4388 .collect::<Vec<_>>();
4389 let compiler_sources = semantic
4390 .functions
4391 .iter()
4392 .filter_map(|function| {
4393 let source = closure_for_body_span(&function.body_span, &closures, file_identities)?;
4394 let marker = compiler_closure_marker(&function.definition_name)?;
4395 Some((marker, (source.identity.clone(), source.lambda.clone())))
4396 })
4397 .collect::<HashMap<_, _>>();
4398
4399 let mut direct_closures = HashMap::<String, (String, String)>::new();
4400 for function in &semantic.functions {
4401 let Some(source) = closure_for_body_span(&function.body_span, &closures, file_identities)
4402 else {
4403 continue;
4404 };
4405 let located_key = canonicalize_closure_value(
4406 &function.key,
4407 &closures,
4408 file_identities,
4409 ClosureRewrite::Identity,
4410 &compiler_sources,
4411 );
4412 let key = replace_closure_definition(&located_key, &source.identity);
4413 let display = canonicalize_closure_value(
4414 &closure_instance_display(&source.lambda, &function.display),
4415 &closures,
4416 file_identities,
4417 ClosureRewrite::Display,
4418 &compiler_sources,
4419 );
4420 direct_closures.insert(function.key.clone(), (key, display));
4421 }
4422
4423 for function in &mut semantic.functions {
4424 let original_key = function.key.clone();
4425 if let Some((key, display)) = direct_closures.get(&original_key) {
4426 function.key = key.clone();
4427 function.display = display.clone();
4428 } else {
4429 function.key = canonicalize_closure_value(
4430 &function.key,
4431 &closures,
4432 file_identities,
4433 ClosureRewrite::Identity,
4434 &compiler_sources,
4435 );
4436 function.display = canonicalize_closure_value(
4437 &function.display,
4438 &closures,
4439 file_identities,
4440 ClosureRewrite::Display,
4441 &compiler_sources,
4442 );
4443 }
4444 for parameter in &mut function.parameter_types {
4445 *parameter = canonicalize_closure_value(
4446 parameter,
4447 &closures,
4448 file_identities,
4449 ClosureRewrite::Display,
4450 &compiler_sources,
4451 );
4452 }
4453 for call in &mut function.calls {
4454 for argument in &mut call.argument_types {
4455 *argument = canonicalize_closure_value(
4456 argument,
4457 &closures,
4458 file_identities,
4459 ClosureRewrite::Display,
4460 &compiler_sources,
4461 );
4462 }
4463 match &mut call.target {
4464 SemanticCallTarget::Direct { key, display } => {
4465 rewrite_closure_target(
4466 key,
4467 display,
4468 &direct_closures,
4469 &closures,
4470 file_identities,
4471 &compiler_sources,
4472 );
4473 }
4474 SemanticCallTarget::Dynamic {
4475 key,
4476 display,
4477 candidates,
4478 ..
4479 } => {
4480 rewrite_closure_target(
4481 key,
4482 display,
4483 &direct_closures,
4484 &closures,
4485 file_identities,
4486 &compiler_sources,
4487 );
4488 for candidate in candidates {
4489 rewrite_closure_target(
4490 &mut candidate.key,
4491 &mut candidate.display,
4492 &direct_closures,
4493 &closures,
4494 file_identities,
4495 &compiler_sources,
4496 );
4497 }
4498 }
4499 SemanticCallTarget::Indirect { .. } => {}
4500 SemanticCallTarget::Unresolved => {}
4501 }
4502 }
4503 }
4504}
4505
4506fn is_source_closure(function: &FunctionInfo) -> bool {
4507 function.id.name.starts_with("{lambda:")
4508}
4509
4510fn rewrite_closure_target(
4511 key: &mut String,
4512 display: &mut String,
4513 direct_closures: &HashMap<String, (String, String)>,
4514 closures: &[SemanticClosureSource],
4515 file_identities: &HashMap<PathBuf, PathBuf>,
4516 compiler_sources: &HashMap<String, (String, String)>,
4517) {
4518 if let Some((canonical_key, canonical_display)) = direct_closures.get(key) {
4519 *key = canonical_key.clone();
4520 *display = canonical_display.clone();
4521 return;
4522 }
4523 *key = canonicalize_closure_value(
4524 key,
4525 closures,
4526 file_identities,
4527 ClosureRewrite::Identity,
4528 compiler_sources,
4529 );
4530 *display = canonicalize_closure_value(
4531 display,
4532 closures,
4533 file_identities,
4534 ClosureRewrite::Display,
4535 compiler_sources,
4536 );
4537}
4538
4539fn canonicalize_closure_value(
4540 value: &str,
4541 closures: &[SemanticClosureSource],
4542 file_identities: &HashMap<PathBuf, PathBuf>,
4543 rewrite: ClosureRewrite,
4544 compiler_sources: &HashMap<String, (String, String)>,
4545) -> String {
4546 let mut value = rewrite_located_closures(value, closures, file_identities, rewrite);
4547 for (marker, (identity, display)) in compiler_sources {
4548 value = value.replace(
4549 marker,
4550 match rewrite {
4551 ClosureRewrite::Identity => identity,
4552 ClosureRewrite::Display => display,
4553 },
4554 );
4555 }
4556 match rewrite {
4557 ClosureRewrite::Identity => value,
4558 ClosureRewrite::Display => {
4559 render_compiler_closure_markers(&strip_closure_environment_contexts(&value))
4560 }
4561 }
4562}
4563
4564fn strip_closure_environment_contexts(value: &str) -> String {
4565 const PREFIX: &str = "[[diffkit-closure-env:";
4566 let mut output = String::with_capacity(value.len());
4567 let mut remainder = value;
4568 while let Some(start) = remainder.find(PREFIX) {
4569 output.push_str(&remainder[..start]);
4570 let context = &remainder[start + PREFIX.len()..];
4571 let Some(end) = context.find("]]") else {
4572 output.push_str(&remainder[start..]);
4573 return output;
4574 };
4575 remainder = &context[end + 2..];
4576 }
4577 output.push_str(remainder);
4578 output
4579}
4580
4581fn render_compiler_closure_markers(value: &str) -> String {
4582 let mut output = String::with_capacity(value.len());
4583 let mut remainder = value;
4584 while let Some(start) = remainder.find("{lambda-def:") {
4585 output.push_str(&remainder[..start]);
4586 let marker = &remainder[start..];
4587 let Some(end) = marker.find('}') else {
4588 output.push_str(marker);
4589 return output;
4590 };
4591 let label = marker[..end]
4592 .rsplit_once(':')
4593 .map_or(&marker[..=end], |(_, label)| label);
4594 output.push_str(label);
4595 remainder = &marker[end + 1..];
4596 }
4597 output.push_str(remainder);
4598 output
4599}
4600
4601fn closure_for_body_span<'a>(
4602 body: &SourceSpan,
4603 closures: &'a [SemanticClosureSource],
4604 file_identities: &HashMap<PathBuf, PathBuf>,
4605) -> Option<&'a SemanticClosureSource> {
4606 closures
4607 .iter()
4608 .filter(|closure| {
4609 source_file_identity(file_identities, &closure.span.file)
4610 == source_file_identity(file_identities, &body.file)
4611 && closure.span.contains(body)
4612 })
4613 .min_by_key(|closure| closure.span.extent())
4614}
4615
4616fn rewrite_located_closures(
4617 value: &str,
4618 closures: &[SemanticClosureSource],
4619 file_identities: &HashMap<PathBuf, PathBuf>,
4620 rewrite: ClosureRewrite,
4621) -> String {
4622 let mut output = String::with_capacity(value.len());
4623 let mut remainder = value;
4624 loop {
4625 let located = ["{closure@", "{async closure@"]
4626 .into_iter()
4627 .filter_map(|prefix| remainder.find(prefix).map(|start| (start, prefix)))
4628 .min_by_key(|(start, _)| *start);
4629 let Some((start, prefix)) = located else {
4630 output.push_str(remainder);
4631 break;
4632 };
4633 output.push_str(&remainder[..start]);
4634 let located = &remainder[start..];
4635 let Some(end) = located.find('}') else {
4636 output.push_str(located);
4637 break;
4638 };
4639 let location = &located[prefix.len()..end];
4640 let replacement = parse_anonymous_location(location).and_then(|(file, line, column)| {
4641 closure_for_location(&file, line, column, closures, file_identities)
4642 });
4643 if let Some(closure) = replacement {
4644 output.push_str(match rewrite {
4645 ClosureRewrite::Identity => &closure.identity,
4646 ClosureRewrite::Display => &closure.lambda,
4647 });
4648 } else {
4649 output.push_str(&located[..=end]);
4650 }
4651 remainder = &located[end + 1..];
4652 }
4653 output
4654}
4655
4656fn parse_anonymous_location(location: &str) -> Option<(PathBuf, usize, usize)> {
4657 let mut fields = location.rsplitn(5, ':');
4658 let _end_column = fields.next()?.trim().parse::<usize>().ok()?;
4659 let _end_line = fields.next()?.trim().parse::<usize>().ok()?;
4660 let start_column = fields.next()?.trim().parse::<usize>().ok()?;
4661 let start_line = fields.next()?.trim().parse::<usize>().ok()?;
4662 let file = PathBuf::from(fields.next()?.trim());
4663 Some((file, start_line, start_column))
4664}
4665
4666fn closure_for_location<'a>(
4667 file: &Path,
4668 line: usize,
4669 column: usize,
4670 closures: &'a [SemanticClosureSource],
4671 file_identities: &HashMap<PathBuf, PathBuf>,
4672) -> Option<&'a SemanticClosureSource> {
4673 let candidates = closures
4674 .iter()
4675 .filter(|closure| {
4676 source_paths_match(file_identities, &closure.span.file, file)
4677 && closure.span.start_line == line
4678 })
4679 .collect::<Vec<_>>();
4680 let best_distance = candidates
4681 .iter()
4682 .map(|closure| closure.span.start_column.saturating_add(1).abs_diff(column))
4683 .min()?;
4684 let mut best = candidates.into_iter().filter(|closure| {
4685 closure.span.start_column.saturating_add(1).abs_diff(column) == best_distance
4686 });
4687 let closure = best.next()?;
4688 best.next().is_none().then_some(closure)
4689}
4690
4691fn replace_closure_definition(value: &str, replacement: &str) -> String {
4692 let Some(start) = ["{closure#", "{async closure#"]
4693 .into_iter()
4694 .filter_map(|prefix| value.rfind(prefix))
4695 .max()
4696 else {
4697 return value.to_owned();
4698 };
4699 let Some(end) = value[start..].find('}').map(|end| start + end) else {
4700 return value.to_owned();
4701 };
4702 format!("{}{replacement}{}", &value[..start], &value[end + 1..])
4703}
4704
4705fn closure_instance_display(lambda: &str, semantic: &str) -> String {
4706 let Some(start) = ["::{closure#", "::{async closure#"]
4707 .into_iter()
4708 .filter_map(|prefix| semantic.rfind(prefix))
4709 .max()
4710 else {
4711 return lambda.to_owned();
4712 };
4713 let parent = &semantic[..start];
4714 let Some(generic_start) = trailing_generic_arguments_start(parent) else {
4715 return lambda.to_owned();
4716 };
4717 format!("{}{}", lambda, &parent[generic_start..])
4718}
4719
4720fn source_file_identities(
4721 syntax: &FileAnalysis,
4722 semantic: &SemanticProgram,
4723) -> HashMap<PathBuf, PathBuf> {
4724 let mut paths = HashSet::<PathBuf>::new();
4725 for function in &syntax.functions {
4726 paths.insert(function.span.file.clone());
4727 paths.extend(function.calls.iter().map(|call| call.span.file.clone()));
4728 }
4729 for function in &semantic.functions {
4730 paths.insert(function.body_span.file.clone());
4731 paths.extend(function.calls.iter().map(|call| call.span.file.clone()));
4732 }
4733
4734 paths
4735 .into_iter()
4736 .map(|path| {
4737 let identity = path.canonicalize().unwrap_or_else(|_| {
4738 if path.is_absolute() {
4739 path.clone()
4740 } else {
4741 env::current_dir().map_or_else(|_| path.clone(), |root| root.join(&path))
4742 }
4743 });
4744 (path, identity)
4745 })
4746 .collect()
4747}
4748
4749fn source_file_identity<'a>(identities: &'a HashMap<PathBuf, PathBuf>, path: &'a Path) -> &'a Path {
4750 identities.get(path).map_or(path, PathBuf::as_path)
4751}
4752
4753fn source_paths_match(identities: &HashMap<PathBuf, PathBuf>, left: &Path, right: &Path) -> bool {
4754 let left = source_file_identity(identities, left);
4755 let right = source_file_identity(identities, right);
4756 left == right
4757 || (left.is_absolute() && !right.is_absolute() && left.ends_with(right))
4758 || (right.is_absolute() && !left.is_absolute() && right.ends_with(left))
4759}
4760
4761fn best_function_template<'a>(
4762 functions: &'a [FunctionInfo],
4763 candidate_indices: &[usize],
4764 body_span: &SourceSpan,
4765) -> Option<&'a FunctionInfo> {
4766 candidate_indices
4767 .iter()
4768 .filter_map(|index| functions.get(*index))
4769 .filter(|function| function.span.contains(body_span))
4770 .min_by_key(|function| function.span.extent())
4771}
4772
4773fn best_semantic_call(
4774 syntax_call: &CallSite,
4775 semantic_calls: &[SemanticCall],
4776 claimed: &HashSet<usize>,
4777 file_identities: &HashMap<PathBuf, PathBuf>,
4778) -> Option<usize> {
4779 let syntax_name = call_syntax_name(syntax_call);
4780
4781 semantic_calls
4782 .iter()
4783 .enumerate()
4784 .filter(|(index, call)| {
4785 if claimed.contains(index)
4786 || source_file_identity(file_identities, &syntax_call.span.file)
4787 != source_file_identity(file_identities, &call.span.file)
4788 || !syntax_call.span.overlaps(&call.span)
4789 {
4790 return false;
4791 }
4792 let definition_leaf = call
4793 .definition_name
4794 .rsplit("::")
4795 .next()
4796 .unwrap_or(&call.definition_name);
4797 syntax_name.is_none_or(|syntax_name| definition_leaf == syntax_name)
4798 || syntax_call.span.has_same_coordinates(&call.span)
4799 })
4800 .min_by_key(|(_, call)| {
4801 let definition_leaf = call
4802 .definition_name
4803 .rsplit("::")
4804 .next()
4805 .unwrap_or(&call.definition_name);
4806 let name_penalty = syntax_name
4807 .map(|syntax_name| usize::from(definition_leaf != syntax_name) * 1_000_000)
4808 .unwrap_or_default();
4809 name_penalty + syntax_call.span.start_distance(&call.span)
4810 })
4811 .map(|(index, _)| index)
4812}
4813
4814fn call_syntax_name(call: &CallSite) -> Option<&str> {
4815 match call.syntax.visible() {
4816 CallSyntax::Path(parts) => parts.last().map(String::as_str),
4817 CallSyntax::SelfMethod(method) | CallSyntax::Method { method, .. } => Some(method.as_str()),
4818 CallSyntax::Expression(_) | CallSyntax::CompilerConfirmed(_) => None,
4819 }
4820}
4821
4822fn semantic_symbol(name: String) -> SymbolId {
4823 SymbolId {
4824 language: LanguageId::new("rust"),
4825 module: Vec::new(),
4826 container: None,
4827 name,
4828 }
4829}
4830
4831fn stable_instance_name(instance: Instance) -> String {
4832 let name = instance.name().to_string();
4833 let arguments = instance.args();
4834 let mut replacements = BTreeMap::<String, Option<String>>::new();
4835 collect_compiler_closure_replacements(&arguments, &mut replacements);
4836 let mut stable = replacements
4837 .into_iter()
4838 .filter_map(|(located, marker)| marker.map(|marker| (located, marker)))
4839 .fold(name, |name, (located, marker)| {
4840 name.replace(&located, &marker)
4841 });
4842 if let Some(environment) = closure_environment_context(&arguments) {
4843 stable.push_str("[[diffkit-closure-env:");
4844 stable.push_str(&environment);
4845 stable.push_str("]]");
4846 }
4847 stable
4848}
4849
4850fn closure_environment_context(arguments: &GenericArgs) -> Option<String> {
4863 let mut context = String::new();
4864 let mut active = HashSet::new();
4865 let has_closure = append_closure_environment(arguments, &mut active, &mut context);
4866 has_closure.then_some(context)
4867}
4868
4869fn append_closure_environment(
4870 arguments: &GenericArgs,
4871 active: &mut HashSet<Ty>,
4872 output: &mut String,
4873) -> bool {
4874 let mut has_closure = false;
4875 output.push('(');
4876 for (index, argument) in arguments.0.iter().enumerate() {
4877 if index > 0 {
4878 output.push(',');
4879 }
4880 let GenericArgKind::Type(ty) = argument else {
4881 output.push('_');
4882 continue;
4883 };
4884 let rendered = ty.to_string();
4885 if rendered.starts_with("{closure@")
4886 && let TyKind::RigidTy(RigidTy::Closure(definition, nested)) = ty.kind()
4887 {
4888 has_closure = true;
4889 let marker = compiler_closure_marker(&definition.name().to_string())
4890 .unwrap_or_else(|| "{lambda-def:unknown:λclosure}".to_owned());
4891 output.push_str(&marker);
4892 if active.insert(*ty) {
4893 append_closure_environment(&nested, active, output);
4894 active.remove(ty);
4895 }
4896 continue;
4897 }
4898
4899 let normalized = normalize_type_identity_shape(&rendered);
4900 let mut hasher = Sha256::new();
4901 hasher.update(normalized.as_bytes());
4902 let digest = format!("{:x}", hasher.finalize());
4903 output.push_str("t:");
4904 output.push_str(&digest[..12]);
4905 }
4906 output.push(')');
4907 has_closure
4908}
4909
4910fn normalize_type_identity_shape(value: &str) -> String {
4911 [
4912 ("{closure@", "{closure}"),
4913 ("{async closure body@", "{async closure body}"),
4914 ("{async closure@", "{async closure}"),
4915 ("{async block@", "{async block}"),
4916 ("{coroutine@", "{coroutine}"),
4917 ("{generator@", "{generator}"),
4918 ]
4919 .into_iter()
4920 .fold(value.to_owned(), |value, (prefix, replacement)| {
4921 replace_braced_location(&value, prefix, replacement)
4922 })
4923}
4924
4925fn collect_compiler_closure_replacements(
4926 arguments: &GenericArgs,
4927 replacements: &mut BTreeMap<String, Option<String>>,
4928) {
4929 for argument in &arguments.0 {
4930 if let GenericArgKind::Type(ty) = argument {
4931 collect_direct_closure_replacement(*ty, replacements);
4932 }
4933 }
4934}
4935
4936fn collect_direct_closure_replacement(ty: Ty, replacements: &mut BTreeMap<String, Option<String>>) {
4937 let rendered = ty.to_string();
4943 if !rendered.starts_with("{closure@") {
4944 return;
4945 }
4946 let TyKind::RigidTy(RigidTy::Closure(definition, _)) = ty.kind() else {
4947 return;
4948 };
4949 record_compiler_closure(&rendered, &definition.name().to_string(), replacements);
4950}
4951
4952fn record_compiler_closure(
4953 rendered: &str,
4954 definition_name: &str,
4955 replacements: &mut BTreeMap<String, Option<String>>,
4956) {
4957 let Some(located) = located_closure_token(rendered) else {
4958 return;
4959 };
4960 let Some(marker) = compiler_closure_marker(definition_name) else {
4961 return;
4962 };
4963 replacements
4964 .entry(located.to_owned())
4965 .and_modify(|current| {
4966 if current.as_deref() != Some(&marker) {
4967 *current = None;
4968 }
4969 })
4970 .or_insert(Some(marker));
4971}
4972
4973fn located_closure_token(value: &str) -> Option<&str> {
4974 let start = ["{closure@", "{async closure@"]
4975 .into_iter()
4976 .filter_map(|prefix| value.find(prefix))
4977 .min()?;
4978 let end = value[start..].find('}')? + start;
4979 Some(&value[start..=end])
4980}
4981
4982fn compiler_closure_marker(definition_name: &str) -> Option<String> {
4983 let label = compiler_closure_label(definition_name)?;
4984 let mut hasher = Sha256::new();
4985 hasher.update(definition_name.as_bytes());
4986 let identity = format!("{:x}", hasher.finalize());
4987 Some(format!("{{lambda-def:{}:{label}}}", &identity[..12]))
4988}
4989
4990fn compiler_closure_label(definition_name: &str) -> Option<String> {
4991 let (parent, ordinal) = ["::{closure#", "::{async closure#"]
4992 .into_iter()
4993 .find_map(|separator| definition_name.rsplit_once(separator))?;
4994 let ordinal = ordinal.strip_suffix('}')?.parse::<usize>().ok()? + 1;
4995 let parent = parent.rsplit("::").next().unwrap_or("closure");
4996 let parent = parent
4997 .chars()
4998 .map(|character| {
4999 if character.is_ascii_alphanumeric() || character == '_' {
5000 character
5001 } else {
5002 '_'
5003 }
5004 })
5005 .collect::<String>();
5006 let parent = if parent.trim_matches('_').is_empty() {
5007 "closure"
5008 } else {
5009 parent.trim_matches('_')
5010 };
5011 Some(format!("λ{parent}#{ordinal}"))
5012}
5013
5014fn normalize_instance_key(name: &str) -> String {
5015 normalize_anonymous_type_locations(&name.replace("::<", "<"))
5016}
5017
5018fn normalize_instance_display(name: &str, crate_name: &str) -> String {
5019 let without_crate = if crate_name.is_empty() {
5020 name.to_owned()
5021 } else {
5022 name.replace(&format!("{crate_name}::"), "")
5023 };
5024 let compact = normalize_anonymous_type_locations(&without_crate.replace("::<", "<"));
5025 collapse_trait_qualification(&compact)
5026}
5027
5028fn normalize_anonymous_type_locations(value: &str) -> String {
5029 [
5030 ("{async closure body@", "{async closure body}"),
5031 ("{async block@", "{async block}"),
5032 ("{coroutine@", "{coroutine}"),
5033 ("{generator@", "{generator}"),
5034 ]
5035 .into_iter()
5036 .fold(value.to_owned(), |value, (prefix, replacement)| {
5037 replace_braced_location(&value, prefix, replacement)
5038 })
5039}
5040
5041fn replace_braced_location(value: &str, prefix: &str, replacement: &str) -> String {
5042 let mut output = String::with_capacity(value.len());
5043 let mut remainder = value;
5044 while let Some(start) = remainder.find(prefix) {
5045 output.push_str(&remainder[..start]);
5046 let located = &remainder[start..];
5047 let Some(end) = located.find('}') else {
5048 output.push_str(located);
5049 return output;
5050 };
5051 output.push_str(replacement);
5052 remainder = &located[end + 1..];
5053 }
5054 output.push_str(remainder);
5055 output
5056}
5057
5058fn collapse_trait_qualification(name: &str) -> String {
5059 let Some(rest) = name.strip_prefix('<') else {
5060 return name.to_owned();
5061 };
5062 let Some((qualification, method)) = rest.rsplit_once(">::") else {
5063 return name.to_owned();
5064 };
5065 let Some((self_type, _trait_name)) = qualification.rsplit_once(" as ") else {
5066 return name.to_owned();
5067 };
5068 format!("{self_type}::{method}")
5069}
5070
5071fn replace_label_callee(label: &CallLabel, callee: &str) -> CallLabel {
5072 CallLabel {
5073 default: replace_callee_text(&label.default, callee),
5074 typed: label
5075 .typed
5076 .as_ref()
5077 .map(|typed| replace_callee_text(typed, callee)),
5078 }
5079}
5080
5081fn replace_dispatch_candidate_label(label: &CallLabel, callee: &str) -> CallLabel {
5082 let rewrite = |text: &str| {
5083 let candidate = dispatch_candidate_callee(callee);
5084 outer_call_arguments_start(text).map_or(candidate.clone(), |arguments_start| {
5085 format!("{candidate}{}", &text[arguments_start..])
5086 })
5087 };
5088 CallLabel {
5089 default: rewrite(&label.default),
5090 typed: label.typed.as_deref().map(rewrite),
5091 }
5092}
5093
5094fn dispatch_candidate_callee(callee: &str) -> String {
5095 let callee = collapse_trait_qualification(callee);
5096 if callee.starts_with('λ') {
5097 return callee;
5098 }
5099 let segments = split_top_level_path(&callee);
5100 let start = segments.len().saturating_sub(2);
5101 segments[start..]
5102 .iter()
5103 .map(|segment| simplify_type_paths(segment))
5104 .collect::<Vec<_>>()
5105 .join("::")
5106}
5107
5108fn replace_callee_text(label: &str, callee: &str) -> String {
5109 if let Some(arguments_start) = outer_call_arguments_start(label) {
5110 format!(
5111 "{}{}",
5112 semantic_callee(&label[..arguments_start], callee),
5113 &label[arguments_start..]
5114 )
5115 } else {
5116 semantic_callee(label, callee)
5117 }
5118}
5119
5120fn semantic_callee(source: &str, semantic: &str) -> String {
5121 if semantic.starts_with('λ') {
5122 return semantic.to_owned();
5123 }
5124 if source.contains('<') && source.contains('λ') {
5125 return source.to_owned();
5130 }
5131 if semantic.starts_with("dyn ") {
5132 return semantic.rsplit_once("::").map_or_else(
5133 || semantic.to_owned(),
5134 |(dispatch, method)| format!("{}::{method}", simplify_type_paths(dispatch)),
5135 );
5136 }
5137
5138 if semantic.contains('<') {
5139 let source_segments = split_top_level_path(source);
5140 let semantic_segments = split_top_level_path(semantic);
5141 let keep = source_segments.len().min(semantic_segments.len()).max(1);
5142 return semantic_segments[semantic_segments.len().saturating_sub(keep)..]
5143 .iter()
5144 .map(|segment| simplify_type_paths(segment))
5145 .collect::<Vec<_>>()
5146 .join("::");
5147 }
5148
5149 let parts = semantic.split("::").collect::<Vec<_>>();
5150 let source_receiver = source
5151 .rsplit_once("::")
5152 .map(|(receiver, _)| receiver.rsplit("::").next().unwrap_or(receiver));
5153 let source_requests_concrete_receiver = source.contains('.')
5154 || source.starts_with('<')
5155 || source_receiver.is_some_and(|receiver| {
5156 receiver == "Self" || receiver.chars().next().is_some_and(char::is_uppercase)
5157 });
5158 if parts.len() >= 2
5159 && (source_requests_concrete_receiver
5160 || parts[parts.len() - 2]
5161 .chars()
5162 .next()
5163 .is_some_and(char::is_uppercase))
5164 {
5165 return format!("{}::{}", parts[parts.len() - 2], parts[parts.len() - 1]);
5166 }
5167 source.to_owned()
5168}
5169
5170fn split_top_level_path(value: &str) -> Vec<&str> {
5171 let mut segments = Vec::new();
5172 let mut depth = 0usize;
5173 let mut start = 0usize;
5174 let bytes = value.as_bytes();
5175 let mut index = 0usize;
5176 while index < bytes.len() {
5177 match bytes[index] {
5178 b'<' => depth += 1,
5179 b'>' => depth = depth.saturating_sub(1),
5180 b':' if depth == 0 && bytes.get(index + 1) == Some(&b':') => {
5181 segments.push(&value[start..index]);
5182 index += 1;
5183 start = index + 1;
5184 }
5185 _ => {}
5186 }
5187 index += 1;
5188 }
5189 segments.push(&value[start..]);
5190 segments
5191}
5192
5193fn simplify_type_paths(value: &str) -> String {
5194 let mut output = String::with_capacity(value.len());
5195 let mut token = String::new();
5196 let flush = |token: &mut String, output: &mut String| {
5197 if token.is_empty() {
5198 return;
5199 }
5200 output.push_str(token.rsplit("::").next().unwrap_or(token));
5201 token.clear();
5202 };
5203 for character in value.chars() {
5204 if character.is_ascii_alphanumeric() || matches!(character, '_' | ':' | '\'') {
5205 token.push(character);
5206 } else {
5207 flush(&mut token, &mut output);
5208 output.push(character);
5209 }
5210 }
5211 flush(&mut token, &mut output);
5212 output
5213}
5214
5215fn normalize_type_display(value: &str, crate_name: &str) -> String {
5216 simplify_type_paths(&normalize_anonymous_type_locations(
5217 &value.replace(&format!("{crate_name}::"), ""),
5218 ))
5219}
5220
5221fn semantic_call_argument_types(call: &SemanticCall) -> Vec<String> {
5222 let is_closure = matches!(
5223 &call.target,
5224 SemanticCallTarget::Direct { display, .. }
5225 if display.starts_with('λ') || display.contains("{closure#")
5226 );
5227 if !is_closure {
5228 return call.argument_types.clone();
5229 }
5230 let Some(tuple) = call.argument_types.last() else {
5231 return Vec::new();
5232 };
5233 let Some(tuple) = tuple
5234 .strip_prefix('(')
5235 .and_then(|tuple| tuple.strip_suffix(')'))
5236 else {
5237 return call.argument_types.clone();
5238 };
5239 split_rust_arguments(tuple)
5240 .into_iter()
5241 .filter(|argument| !argument.is_empty())
5242 .map(ToOwned::to_owned)
5243 .collect()
5244}
5245
5246fn annotate_rust_label(label: &str, semantic_types: &[String]) -> String {
5247 let Some(arguments_start) = outer_call_arguments_start(label) else {
5248 return label.to_owned();
5249 };
5250 let Some(arguments_end) = matching_parenthesis(label, arguments_start) else {
5251 return label.to_owned();
5252 };
5253 let arguments = split_rust_arguments(&label[arguments_start + 1..arguments_end]);
5254 if arguments.is_empty() || semantic_types.is_empty() {
5255 return label.to_owned();
5256 }
5257 let types = if semantic_types.len() >= arguments.len() {
5258 &semantic_types[semantic_types.len() - arguments.len()..]
5259 } else {
5260 semantic_types
5261 };
5262 let leading_untyped = arguments.len().saturating_sub(types.len());
5263 let annotated = arguments
5264 .iter()
5265 .enumerate()
5266 .map(|(index, argument)| {
5267 if index < leading_untyped {
5268 argument.to_string()
5269 } else {
5270 format!("{}: {}", argument, types[index - leading_untyped])
5271 }
5272 })
5273 .collect::<Vec<_>>()
5274 .join(", ");
5275 format!(
5276 "{}({annotated}){}",
5277 &label[..arguments_start],
5278 &label[arguments_end + 1..]
5279 )
5280}
5281
5282fn matching_parenthesis(label: &str, start: usize) -> Option<usize> {
5283 let mut depth = 0usize;
5284 let mut in_string = false;
5285 let mut escaped = false;
5286 for (offset, character) in label[start..].char_indices() {
5287 if in_string {
5288 if escaped {
5289 escaped = false;
5290 } else if character == '\\' {
5291 escaped = true;
5292 } else if character == '"' {
5293 in_string = false;
5294 }
5295 continue;
5296 }
5297 match character {
5298 '"' => in_string = true,
5299 '(' => depth += 1,
5300 ')' => {
5301 depth = depth.checked_sub(1)?;
5302 if depth == 0 {
5303 return Some(start + offset);
5304 }
5305 }
5306 _ => {}
5307 }
5308 }
5309 None
5310}
5311
5312fn split_rust_arguments(arguments: &str) -> Vec<&str> {
5313 if arguments.trim().is_empty() {
5314 return Vec::new();
5315 }
5316 let mut result = Vec::new();
5317 let mut start = 0usize;
5318 let mut delimiters = Vec::new();
5319 let mut in_string = false;
5320 let mut escaped = false;
5321 for (index, character) in arguments.char_indices() {
5322 if in_string {
5323 if escaped {
5324 escaped = false;
5325 } else if character == '\\' {
5326 escaped = true;
5327 } else if character == '"' {
5328 in_string = false;
5329 }
5330 continue;
5331 }
5332 match character {
5333 '"' => in_string = true,
5334 '(' | '[' | '{' | '<' => delimiters.push(character),
5335 ')' | ']' | '}' | '>' => {
5336 delimiters.pop();
5337 }
5338 ',' if delimiters.is_empty() => {
5339 result.push(arguments[start..index].trim());
5340 start = index + character.len_utf8();
5341 }
5342 _ => {}
5343 }
5344 }
5345 result.push(arguments[start..].trim());
5346 result
5347}
5348
5349fn outer_call_arguments_start(label: &str) -> Option<usize> {
5350 let mut depth = 0usize;
5351 for (index, character) in label.char_indices().rev() {
5352 match character {
5353 ')' => depth += 1,
5354 '(' => {
5355 depth = depth.checked_sub(1)?;
5356 if depth == 0 {
5357 return Some(index);
5358 }
5359 }
5360 _ => {}
5361 }
5362 }
5363 None
5364}
5365
5366fn rustc_source_span(span: rustc_public::ty::Span) -> SourceSpan {
5367 let lines = span.get_lines();
5368 let file = PathBuf::from(span.get_filename());
5369 let file = if file.is_absolute() {
5370 file
5371 } else {
5372 env::current_dir().map_or(file.clone(), |directory| directory.join(file))
5373 };
5374 SourceSpan {
5375 file,
5376 start_line: lines.start_line,
5377 start_column: lines.start_col.saturating_sub(1),
5378 start_byte: None,
5379 end_line: lines.end_line.max(lines.start_line),
5380 end_column: lines.end_col.saturating_sub(1),
5381 end_byte: None,
5382 }
5383}
5384
5385fn source_files_match(left: &Path, right: &Path) -> bool {
5386 paths_match(left, right)
5387}
5388
5389impl LanguageBackend for RustBackend {
5390 fn language(&self) -> LanguageId {
5391 LanguageId::new("rust")
5392 }
5393
5394 fn extensions(&self) -> &'static [&'static str] {
5395 &["rs"]
5396 }
5397
5398 fn analyze_file(
5399 &self,
5400 context: &FileContext<'_>,
5401 source: &str,
5402 ) -> FrontendResult<FileAnalysis> {
5403 let syntax = syn::parse_file(source)?;
5404 let mut analysis = FileAnalysis::default();
5405 analysis.source_files.insert(context.path.to_path_buf());
5406 analyze_items(context.path, context.module, &syntax.items, &mut analysis);
5407 Ok(analysis)
5408 }
5409
5410 fn analyze_project(&self, context: &ProjectContext<'_>) -> FrontendResult<FileAnalysis> {
5411 let wrapper = context.driver_executable.ok_or_else(|| {
5412 std::io::Error::other("Rust project analysis requires a driver executable")
5413 })?;
5414 let session = match context.cache {
5415 Some(cache) => RustProjectSession::create_cached(
5416 cache.project_root,
5417 cache.endpoint,
5418 cache.verbose,
5419 )?,
5420 None => RustProjectSession::create()?,
5421 };
5422 analyze_semantic_project(context.root, wrapper, context.entries, &session)
5423 }
5424}
5425
5426fn analyze_items(file: &Path, module: &[String], items: &[Item], analysis: &mut FileAnalysis) {
5427 for item in items {
5428 match item {
5429 Item::Fn(function) => {
5430 let function_info = function_from_item(file, module, None, function);
5431 let owner = function_info.id.clone();
5432 add_signature_facts(file, &function_info, &function.sig, analysis);
5433 analysis.functions.push(function_info);
5434 add_closure_functions(file, module, &owner, &function.block, analysis);
5435 }
5436 Item::Impl(item_impl) => analyze_impl(file, module, item_impl, analysis),
5437 Item::Trait(item_trait) => analyze_trait(file, module, item_trait, analysis),
5438 Item::Mod(item_mod) => {
5439 if let Some((_, nested)) = &item_mod.content {
5440 let mut nested_module = module.to_vec();
5441 nested_module.push(item_mod.ident.to_string());
5442 analyze_items(file, &nested_module, nested, analysis);
5443 }
5444 }
5445 _ => {}
5446 }
5447 }
5448}
5449
5450fn function_from_item(
5451 file: &Path,
5452 module: &[String],
5453 container: Option<String>,
5454 function: &ItemFn,
5455) -> FunctionInfo {
5456 function_from_parts(
5457 file,
5458 module,
5459 container,
5460 &function.sig,
5461 &function.block,
5462 is_public(&function.vis),
5463 function.span(),
5464 )
5465}
5466
5467fn analyze_impl(file: &Path, module: &[String], item_impl: &ItemImpl, analysis: &mut FileAnalysis) {
5468 let self_ty = compact_tokens(&item_impl.self_ty);
5469 let trait_name = item_impl
5470 .trait_
5471 .as_ref()
5472 .map(|(_, path, _)| compact_tokens(path));
5473 let container = trait_name
5474 .as_ref()
5475 .map(|trait_name| format!("{self_ty} as {trait_name}"))
5476 .unwrap_or_else(|| self_ty.clone());
5477
5478 for item in &item_impl.items {
5479 let ImplItem::Fn(method) = item else {
5480 continue;
5481 };
5482 let function = function_from_parts(
5483 file,
5484 module,
5485 Some(container.clone()),
5486 &method.sig,
5487 &method.block,
5488 is_public(&method.vis) || trait_name.is_some(),
5489 method.span(),
5490 );
5491 let owner = function.id.clone();
5492
5493 add_signature_facts(file, &function, &method.sig, analysis);
5494 if let Some(trait_name) = &trait_name {
5495 analysis.facts.push(LanguageFact {
5496 subject: function.id.clone(),
5497 namespace: LanguageId::new("rust"),
5498 kind: "impl-trait".to_owned(),
5499 key: trait_name.clone(),
5500 value: self_ty.clone(),
5501 span: function.span.clone(),
5502 });
5503 }
5504 analysis.functions.push(function);
5505 add_closure_functions(file, module, &owner, &method.block, analysis);
5506 }
5507}
5508
5509fn analyze_trait(
5510 file: &Path,
5511 module: &[String],
5512 item_trait: &ItemTrait,
5513 analysis: &mut FileAnalysis,
5514) {
5515 let trait_name = item_trait.ident.to_string();
5516 for item in &item_trait.items {
5517 let TraitItem::Fn(method) = item else {
5518 continue;
5519 };
5520 let Some(block) = &method.default else {
5521 continue;
5522 };
5523 let function = function_from_parts(
5524 file,
5525 module,
5526 Some(trait_name.clone()),
5527 &method.sig,
5528 block,
5529 is_public(&item_trait.vis),
5530 method.span(),
5531 );
5532 let owner = function.id.clone();
5533 add_signature_facts(file, &function, &method.sig, analysis);
5534 analysis.functions.push(function);
5535 add_closure_functions(file, module, &owner, block, analysis);
5536 }
5537}
5538
5539fn add_closure_functions(
5540 file: &Path,
5541 module: &[String],
5542 owner: &SymbolId,
5543 block: &syn::Block,
5544 analysis: &mut FileAnalysis,
5545) {
5546 let closure_sources = closure_sources(block);
5547 let mut extractor = ClosureExtractor {
5548 file,
5549 module,
5550 owner,
5551 closure_sources: &closure_sources,
5552 functions: Vec::new(),
5553 };
5554 extractor.visit_block(block);
5555 analysis.functions.extend(extractor.functions);
5556}
5557
5558struct ClosureExtractor<'a> {
5559 file: &'a Path,
5560 module: &'a [String],
5561 owner: &'a SymbolId,
5562 closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
5563 functions: Vec<FunctionInfo>,
5564}
5565
5566impl<'ast> Visit<'ast> for ClosureExtractor<'_> {
5567 fn visit_expr_closure(&mut self, node: &'ast ExprClosure) {
5568 let source = self
5569 .closure_sources
5570 .get(&closure_span(node.span()))
5571 .cloned()
5572 .unwrap_or_else(|| ClosureSource::anonymous(self.functions.len()));
5573 let closure_name = format!("{{lambda:{}}}", source.identity);
5574 let lambda = source.lambda_name();
5575 let id = SymbolId {
5576 language: LanguageId::new("rust"),
5577 module: self.module.to_vec(),
5578 container: Some(self.owner.qualified_parts().join("::")),
5579 name: closure_name.clone(),
5580 };
5581 let mut calls = CallCollector {
5582 file: self.file,
5583 closure_sources: self.closure_sources,
5584 calls: Vec::new(),
5585 compiler_confirmation_required: false,
5586 };
5587 calls.visit_expr(&node.body);
5588 let parameters = node.inputs.iter().map(pattern_name).collect::<Vec<_>>();
5589 let typed_parameters = node
5590 .inputs
5591 .iter()
5592 .map(|parameter| match parameter {
5593 Pat::Type(typed) => format!(
5594 "{}: {}",
5595 pattern_name(&typed.pat),
5596 compact_tokens(&typed.ty)
5597 ),
5598 parameter => pattern_name(parameter),
5599 })
5600 .collect::<Vec<_>>();
5601 self.functions.push(FunctionInfo {
5602 id: id.clone(),
5603 label: CallLabel::with_types(
5604 format!("{lambda}({})", parameters.join(", ")),
5605 format!("{lambda}({})", typed_parameters.join(", ")),
5606 ),
5607 public: false,
5608 calls: calls.calls,
5609 span: source_span(self.file, node.span()),
5610 });
5611 let mut nested = ClosureExtractor {
5612 file: self.file,
5613 module: self.module,
5614 owner: &id,
5615 closure_sources: self.closure_sources,
5616 functions: Vec::new(),
5617 };
5618 visit::visit_expr_closure(&mut nested, node);
5619 self.functions.extend(nested.functions);
5620 }
5621
5622 fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
5623
5624 fn visit_macro(&mut self, node: &'ast Macro) {
5625 let Some(expressions) = evaluated_macro_expressions(node) else {
5626 return;
5627 };
5628 for expression in &expressions {
5629 self.visit_expr(expression);
5630 }
5631 }
5632}
5633
5634fn function_from_parts(
5635 file: &Path,
5636 module: &[String],
5637 container: Option<String>,
5638 signature: &Signature,
5639 block: &syn::Block,
5640 public: bool,
5641 span: Span,
5642) -> FunctionInfo {
5643 let id = SymbolId {
5644 language: LanguageId::new("rust"),
5645 module: module.to_vec(),
5646 container,
5647 name: signature.ident.to_string(),
5648 };
5649 let closure_sources = closure_sources(block);
5650 let mut collector = CallCollector {
5651 file,
5652 closure_sources: &closure_sources,
5653 calls: Vec::new(),
5654 compiler_confirmation_required: false,
5655 };
5656 collector.visit_block(block);
5657 rewrite_symbolic_generic_calls(module, signature, &mut collector.calls);
5658
5659 FunctionInfo {
5660 label: function_label(&id, signature),
5661 id,
5662 public,
5663 calls: collector.calls,
5664 span: source_span(file, span),
5665 }
5666}
5667
5668fn add_signature_facts(
5669 file: &Path,
5670 function: &FunctionInfo,
5671 signature: &Signature,
5672 analysis: &mut FileAnalysis,
5673) {
5674 for parameter in &signature.generics.params {
5675 let (key, value) = match parameter {
5676 syn::GenericParam::Lifetime(parameter) => (
5677 parameter.lifetime.to_string(),
5678 parameter
5679 .bounds
5680 .iter()
5681 .map(ToString::to_string)
5682 .collect::<Vec<_>>()
5683 .join(" + "),
5684 ),
5685 syn::GenericParam::Type(parameter) => (
5686 parameter.ident.to_string(),
5687 parameter
5688 .bounds
5689 .iter()
5690 .map(compact_tokens)
5691 .collect::<Vec<_>>()
5692 .join(" + "),
5693 ),
5694 syn::GenericParam::Const(parameter) => {
5695 (parameter.ident.to_string(), compact_tokens(¶meter.ty))
5696 }
5697 };
5698 analysis.facts.push(LanguageFact {
5699 subject: function.id.clone(),
5700 namespace: LanguageId::new("rust"),
5701 kind: "generic-parameter".to_owned(),
5702 key,
5703 value,
5704 span: source_span(file, parameter.span()),
5705 });
5706 }
5707 for input in &signature.inputs {
5708 match input {
5709 FnArg::Receiver(receiver) => {
5710 let mode = match (&receiver.reference, receiver.mutability.is_some()) {
5711 (Some(_), true) => "&mut self",
5712 (Some(_), false) => "&self",
5713 (None, true) => "mut self",
5714 (None, false) => "self",
5715 };
5716 analysis.facts.push(LanguageFact {
5717 subject: function.id.clone(),
5718 namespace: LanguageId::new("rust"),
5719 kind: "receiver".to_owned(),
5720 key: "self".to_owned(),
5721 value: mode.to_owned(),
5722 span: source_span(file, receiver.span()),
5723 });
5724 }
5725 FnArg::Typed(argument) => {
5726 analysis.facts.push(LanguageFact {
5727 subject: function.id.clone(),
5728 namespace: LanguageId::new("rust"),
5729 kind: "parameter".to_owned(),
5730 key: compact_tokens(&argument.pat),
5731 value: compact_tokens(&argument.ty),
5732 span: source_span(file, argument.span()),
5733 });
5734 }
5735 }
5736 }
5737
5738 if signature.asyncness.is_some() {
5739 push_modifier_fact(function, "async", file, signature.span(), analysis);
5740 }
5741 if signature.unsafety.is_some() {
5742 push_modifier_fact(function, "unsafe", file, signature.span(), analysis);
5743 }
5744 if signature.constness.is_some() {
5745 push_modifier_fact(function, "const", file, signature.span(), analysis);
5746 }
5747 if let ReturnType::Type(_, ty) = &signature.output {
5748 analysis.facts.push(LanguageFact {
5749 subject: function.id.clone(),
5750 namespace: LanguageId::new("rust"),
5751 kind: "return-type".to_owned(),
5752 key: "return".to_owned(),
5753 value: compact_tokens(ty),
5754 span: source_span(file, ty.span()),
5755 });
5756 }
5757}
5758
5759fn push_modifier_fact(
5760 function: &FunctionInfo,
5761 modifier: &str,
5762 file: &Path,
5763 span: Span,
5764 analysis: &mut FileAnalysis,
5765) {
5766 analysis.facts.push(LanguageFact {
5767 subject: function.id.clone(),
5768 namespace: LanguageId::new("rust"),
5769 kind: "modifier".to_owned(),
5770 key: modifier.to_owned(),
5771 value: "true".to_owned(),
5772 span: source_span(file, span),
5773 });
5774}
5775
5776fn parameter_names(signature: &Signature) -> String {
5777 signature
5778 .inputs
5779 .iter()
5780 .map(|input| match input {
5781 FnArg::Receiver(receiver) => match (&receiver.reference, receiver.mutability.is_some())
5782 {
5783 (Some(_), true) => "&mut self".to_owned(),
5784 (Some(_), false) => "&self".to_owned(),
5785 (None, true) => "mut self".to_owned(),
5786 (None, false) => "self".to_owned(),
5787 },
5788 FnArg::Typed(argument) => pattern_name(&argument.pat),
5789 })
5790 .collect::<Vec<_>>()
5791 .join(", ")
5792}
5793
5794fn function_label(id: &SymbolId, signature: &Signature) -> CallLabel {
5795 let generic_names = generic_parameter_labels(signature);
5796 let generics = if generic_names.is_empty() {
5797 String::new()
5798 } else {
5799 format!("<{}>", generic_names.join(", "))
5800 };
5801 let default = format!(
5802 "{}{generics}({})",
5803 id.short_name(),
5804 parameter_names(signature)
5805 );
5806 let typed = format!(
5807 "{}{generics}({})",
5808 id.short_name(),
5809 typed_parameters(signature)
5810 );
5811 CallLabel::with_types(default, typed)
5812}
5813
5814fn generic_parameter_labels(signature: &Signature) -> Vec<String> {
5815 let mut where_bounds = HashMap::<String, Vec<String>>::new();
5816 if let Some(where_clause) = &signature.generics.where_clause {
5817 for predicate in &where_clause.predicates {
5818 let syn::WherePredicate::Type(predicate) = predicate else {
5819 continue;
5820 };
5821 let key = compact_tokens(&predicate.bounded_ty);
5822 where_bounds
5823 .entry(key)
5824 .or_default()
5825 .extend(predicate.bounds.iter().map(compact_tokens));
5826 }
5827 }
5828
5829 signature
5830 .generics
5831 .params
5832 .iter()
5833 .map(|parameter| match parameter {
5834 syn::GenericParam::Lifetime(parameter) => {
5835 let mut bounds = parameter
5836 .bounds
5837 .iter()
5838 .map(ToString::to_string)
5839 .collect::<Vec<_>>();
5840 bounds.extend(
5841 where_bounds
5842 .get(¶meter.lifetime.to_string())
5843 .cloned()
5844 .unwrap_or_default(),
5845 );
5846 format_generic_parameter(parameter.lifetime.to_string(), bounds)
5847 }
5848 syn::GenericParam::Type(parameter) => {
5849 let name = parameter.ident.to_string();
5850 let mut bounds = parameter
5851 .bounds
5852 .iter()
5853 .map(compact_tokens)
5854 .collect::<Vec<_>>();
5855 bounds.extend(where_bounds.get(&name).cloned().unwrap_or_default());
5856 format_generic_parameter(name, bounds)
5857 }
5858 syn::GenericParam::Const(parameter) => {
5859 format!(
5860 "const {}: {}",
5861 parameter.ident,
5862 compact_tokens(¶meter.ty)
5863 )
5864 }
5865 })
5866 .collect()
5867}
5868
5869fn format_generic_parameter(name: String, mut bounds: Vec<String>) -> String {
5870 bounds.retain(|bound| !bound.is_empty());
5871 bounds.sort();
5872 bounds.dedup();
5873 if bounds.is_empty() {
5874 name
5875 } else {
5876 format!("{name}: {}", bounds.join(" + "))
5877 }
5878}
5879
5880fn rewrite_symbolic_generic_calls(
5881 module: &[String],
5882 signature: &Signature,
5883 calls: &mut [CallSite],
5884) {
5885 let generic_bounds = generic_type_bounds(signature);
5886 if generic_bounds.is_empty() {
5887 return;
5888 }
5889 let receivers = signature
5890 .inputs
5891 .iter()
5892 .filter_map(|input| {
5893 let FnArg::Typed(argument) = input else {
5894 return None;
5895 };
5896 let parameter = simple_pattern_binding(&argument.pat)?;
5897 let generic = simple_generic_type(&argument.ty)?;
5898 generic_bounds
5899 .contains_key(&generic)
5900 .then_some((parameter, generic))
5901 })
5902 .collect::<HashMap<_, _>>();
5903
5904 for call in calls {
5905 let CallSyntax::Method { receiver, method } = call.syntax.visible() else {
5906 continue;
5907 };
5908 let Some(generic) = receivers.get(receiver) else {
5909 continue;
5910 };
5911 let bound = generic_bounds
5912 .get(generic)
5913 .and_then(|bounds| (bounds.len() == 1).then(|| bounds[0].clone()));
5914 call.target = CallTarget::Direct(SymbolId {
5915 language: LanguageId::new("rust"),
5916 module: module.to_vec(),
5917 container: Some(
5918 bound.map_or_else(|| generic.clone(), |bound| format!("{generic} as {bound}")),
5919 ),
5920 name: method.clone(),
5921 });
5922 call.label = replace_label_callee(&call.label, &format!("{generic}::{method}"));
5923 }
5924}
5925
5926fn generic_type_bounds(signature: &Signature) -> HashMap<String, Vec<String>> {
5927 let mut result = HashMap::<String, Vec<String>>::new();
5928 for parameter in &signature.generics.params {
5929 let syn::GenericParam::Type(parameter) = parameter else {
5930 continue;
5931 };
5932 result.insert(
5933 parameter.ident.to_string(),
5934 parameter.bounds.iter().map(compact_tokens).collect(),
5935 );
5936 }
5937 if let Some(where_clause) = &signature.generics.where_clause {
5938 for predicate in &where_clause.predicates {
5939 let syn::WherePredicate::Type(predicate) = predicate else {
5940 continue;
5941 };
5942 let name = compact_tokens(&predicate.bounded_ty);
5943 if let Some(bounds) = result.get_mut(&name) {
5944 bounds.extend(predicate.bounds.iter().map(compact_tokens));
5945 }
5946 }
5947 }
5948 for bounds in result.values_mut() {
5949 bounds.sort();
5950 bounds.dedup();
5951 }
5952 result
5953}
5954
5955fn simple_generic_type(ty: &syn::Type) -> Option<String> {
5956 match ty {
5957 syn::Type::Path(path) if path.qself.is_none() && path.path.segments.len() == 1 => {
5958 Some(path.path.segments[0].ident.to_string())
5959 }
5960 syn::Type::Reference(reference) => simple_generic_type(&reference.elem),
5961 syn::Type::Group(group) => simple_generic_type(&group.elem),
5962 syn::Type::Paren(paren) => simple_generic_type(&paren.elem),
5963 _ => None,
5964 }
5965}
5966
5967fn typed_parameters(signature: &Signature) -> String {
5968 signature
5969 .inputs
5970 .iter()
5971 .map(|input| match input {
5972 FnArg::Receiver(_) => pattern_name_from_argument(input),
5973 FnArg::Typed(argument) => {
5974 format!(
5975 "{}: {}",
5976 pattern_name(&argument.pat),
5977 compact_tokens(&argument.ty)
5978 )
5979 }
5980 })
5981 .collect::<Vec<_>>()
5982 .join(", ")
5983}
5984
5985fn pattern_name_from_argument(argument: &FnArg) -> String {
5986 match argument {
5987 FnArg::Receiver(receiver) => match (&receiver.reference, receiver.mutability.is_some()) {
5988 (Some(_), true) => "&mut self".to_owned(),
5989 (Some(_), false) => "&self".to_owned(),
5990 (None, true) => "mut self".to_owned(),
5991 (None, false) => "self".to_owned(),
5992 },
5993 FnArg::Typed(argument) => pattern_name(&argument.pat),
5994 }
5995}
5996
5997fn pattern_name(pattern: &Pat) -> String {
5998 match pattern {
5999 Pat::Ident(ident) => ident.ident.to_string(),
6000 Pat::Reference(reference) => pattern_name(&reference.pat),
6001 Pat::Wild(_) => "_".to_owned(),
6002 _ => compact_tokens(pattern),
6003 }
6004}
6005
6006fn is_public(visibility: &Visibility) -> bool {
6007 matches!(visibility, Visibility::Public(_))
6008}
6009
6010fn compact_tokens(tokens: &impl ToTokens) -> String {
6011 render_compact_tokens(tokens.to_token_stream())
6012}
6013
6014#[derive(Clone)]
6015enum CompactToken {
6016 Word(String),
6017 Punct(char),
6018 Group(Delimiter, String),
6019}
6020
6021fn render_compact_tokens(stream: TokenStream) -> String {
6022 let tokens = stream
6023 .into_iter()
6024 .map(|token| match token {
6025 TokenTree::Ident(ident) => CompactToken::Word(ident.to_string()),
6026 TokenTree::Literal(literal) => CompactToken::Word(literal.to_string()),
6027 TokenTree::Punct(punct) => CompactToken::Punct(punct.as_char()),
6028 TokenTree::Group(group) => {
6029 CompactToken::Group(group.delimiter(), render_compact_tokens(group.stream()))
6030 }
6031 })
6032 .collect::<Vec<_>>();
6033 let mut rendered = String::new();
6034 for (index, token) in tokens.iter().enumerate() {
6035 if index > 0 && compact_tokens_need_space(&tokens[index - 1], token) {
6036 rendered.push(' ');
6037 }
6038 match token {
6039 CompactToken::Word(word) => rendered.push_str(word),
6040 CompactToken::Punct(punct) => rendered.push(*punct),
6041 CompactToken::Group(delimiter, body) => match delimiter {
6042 Delimiter::Parenthesis => {
6043 rendered.push('(');
6044 rendered.push_str(body);
6045 rendered.push(')');
6046 }
6047 Delimiter::Brace => {
6048 rendered.push('{');
6049 rendered.push_str(body);
6050 rendered.push('}');
6051 }
6052 Delimiter::Bracket => {
6053 rendered.push('[');
6054 rendered.push_str(body);
6055 rendered.push(']');
6056 }
6057 Delimiter::None => rendered.push_str(body),
6058 },
6059 }
6060 }
6061 rendered
6062}
6063
6064fn compact_tokens_need_space(previous: &CompactToken, current: &CompactToken) -> bool {
6065 match (previous, current) {
6066 (CompactToken::Word(_), CompactToken::Word(_))
6067 | (CompactToken::Group(_, _), CompactToken::Word(_)) => true,
6068 (CompactToken::Punct(',' | ';'), CompactToken::Word(_)) => true,
6069 (CompactToken::Word(word), CompactToken::Punct('|')) => {
6070 matches!(word.as_str(), "move" | "async")
6071 }
6072 (CompactToken::Word(_), CompactToken::Group(Delimiter::Brace, _)) => true,
6073 _ => false,
6074 }
6075}
6076
6077fn source_span(file: &Path, span: Span) -> SourceSpan {
6078 let start = span.start();
6079 let end = span.end();
6080 SourceSpan {
6081 file: file.to_path_buf(),
6082 start_line: start.line,
6083 start_column: start.column,
6084 start_byte: None,
6085 end_line: end.line.max(start.line),
6086 end_column: end.column,
6087 end_byte: None,
6088 }
6089}
6090
6091struct CallCollector<'a> {
6092 file: &'a Path,
6093 closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
6094 calls: Vec<CallSite>,
6095 compiler_confirmation_required: bool,
6096}
6097
6098impl<'ast> Visit<'ast> for CallCollector<'_> {
6099 fn visit_expr_call(&mut self, node: &'ast ExprCall) {
6100 visit::visit_expr_call(self, node);
6104 let mut syntax = callable_path(&node.func).map_or_else(
6105 || CallSyntax::Expression(compact_expression(&node.func, self.closure_sources)),
6106 CallSyntax::Path,
6107 );
6108 if self.compiler_confirmation_required {
6109 syntax = CallSyntax::CompilerConfirmed(Box::new(syntax));
6110 }
6111 let span = source_span(self.file, node.span());
6112 self.calls.push(CallSite {
6113 id: CallSiteId::source(&syntax, &span),
6114 syntax,
6115 target: CallTarget::Unresolved,
6116 label: CallLabel::new(call_expression_label(node, self.closure_sources)),
6117 span,
6118 });
6119 }
6120
6121 fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
6122 visit::visit_expr_method_call(self, node);
6123 let method = node.method.to_string();
6124 let mut syntax = if is_self_expr(&node.receiver) {
6125 CallSyntax::SelfMethod(method)
6126 } else {
6127 CallSyntax::Method {
6128 receiver: receiver_name(&node.receiver),
6129 method,
6130 }
6131 };
6132 if self.compiler_confirmation_required {
6133 syntax = CallSyntax::CompilerConfirmed(Box::new(syntax));
6134 }
6135 let span = source_span(self.file, node.span());
6136 self.calls.push(CallSite {
6137 id: CallSiteId::source(&syntax, &span),
6138 syntax,
6139 target: CallTarget::Unresolved,
6140 label: CallLabel::new(method_call_label(node, self.closure_sources)),
6141 span,
6142 });
6143 }
6144
6145 fn visit_expr_closure(&mut self, _node: &'ast ExprClosure) {}
6148
6149 fn visit_macro(&mut self, node: &'ast Macro) {
6150 let Some(expressions) = evaluated_macro_expressions(node) else {
6151 return;
6152 };
6153 let previous = self.compiler_confirmation_required;
6154 self.compiler_confirmation_required = true;
6155 for expression in &expressions {
6156 self.visit_expr(expression);
6157 }
6158 self.compiler_confirmation_required = previous;
6159 }
6160
6161 fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
6162}
6163
6164fn evaluated_macro_expressions(node: &Macro) -> Option<Punctuated<Expr, Token![,]>> {
6165 let name = node.path.segments.last()?.ident.to_string();
6166 if !matches!(
6167 name.as_str(),
6168 "assert"
6169 | "assert_eq"
6170 | "assert_ne"
6171 | "debug_assert"
6172 | "debug_assert_eq"
6173 | "debug_assert_ne"
6174 | "dbg"
6175 | "eprint"
6176 | "eprintln"
6177 | "format"
6178 | "format_args"
6179 | "panic"
6180 | "print"
6181 | "println"
6182 | "vec"
6183 | "write"
6184 | "writeln"
6185 ) {
6186 return None;
6187 }
6188 Punctuated::<Expr, Token![,]>::parse_terminated
6189 .parse2(node.tokens.clone())
6190 .ok()
6191}
6192
6193fn call_expression_label(
6194 node: &ExprCall,
6195 closure_sources: &HashMap<ClosureSpan, ClosureSource>,
6196) -> String {
6197 let function = compact_tokens(&node.func).replace("::<", "<");
6198 let (closure_types, arguments) = call_arguments(&node.args, closure_sources);
6199 let function = append_generic_arguments(&function, &closure_types);
6200 format!("{function}({})", arguments.join(", "))
6201}
6202
6203fn method_call_label(
6204 node: &ExprMethodCall,
6205 closure_sources: &HashMap<ClosureSpan, ClosureSource>,
6206) -> String {
6207 let receiver = simple_receiver(&node.receiver)
6208 .then(|| compact_expression(&node.receiver, closure_sources));
6209 let generics = node
6210 .turbofish
6211 .as_ref()
6212 .map(compact_tokens)
6213 .unwrap_or_default()
6214 .replace("::<", "<");
6215 let (closure_types, arguments) = call_arguments(&node.args, closure_sources);
6216 let method = format!("{}{generics}", node.method);
6217 let callee = receiver.map_or(method.clone(), |receiver| format!("{receiver}.{method}"));
6218 let callee = append_generic_arguments(&callee, &closure_types);
6219 format!("{callee}({})", arguments.join(", "))
6220}
6221
6222type ClosureSpan = (usize, usize, usize, usize);
6223
6224#[derive(Clone, Debug)]
6225struct ClosureSource {
6226 ordinal: usize,
6227 binding: Option<String>,
6228 identity: String,
6229}
6230
6231impl ClosureSource {
6232 fn anonymous(ordinal: usize) -> Self {
6233 Self {
6234 ordinal,
6235 binding: None,
6236 identity: format!("fallback-{ordinal}"),
6237 }
6238 }
6239
6240 fn lambda_name(&self) -> String {
6241 self.binding.as_ref().map_or_else(
6242 || format!("λ#{}", self.ordinal + 1),
6243 |name| format!("λ{name}"),
6244 )
6245 }
6246}
6247
6248fn closure_span(span: Span) -> ClosureSpan {
6249 let start = span.start();
6250 let end = span.end();
6251 (start.line, start.column, end.line, end.column)
6252}
6253
6254fn closure_sources(block: &syn::Block) -> HashMap<ClosureSpan, ClosureSource> {
6255 let mut collector = ClosureOrdinalCollector::default();
6256 collector.visit_block(block);
6257 collector.sources
6258}
6259
6260#[derive(Default)]
6261struct ClosureOrdinalCollector {
6262 sources: HashMap<ClosureSpan, ClosureSource>,
6263 bindings: HashMap<ClosureSpan, String>,
6264 identity_counts: HashMap<String, usize>,
6265}
6266
6267impl<'ast> Visit<'ast> for ClosureOrdinalCollector {
6268 fn visit_local(&mut self, node: &'ast syn::Local) {
6269 if let Some(initializer) = &node.init
6270 && let Some(closure) = direct_closure_expression(&initializer.expr)
6271 && let Some(binding) = simple_pattern_binding(&node.pat)
6272 {
6273 self.bindings.insert(closure_span(closure.span()), binding);
6274 }
6275 visit::visit_local(self, node);
6276 }
6277
6278 fn visit_expr_closure(&mut self, node: &'ast ExprClosure) {
6279 let span = closure_span(node.span());
6280 let ordinal = self.sources.len();
6281 let base_identity = closure_structural_identity(node);
6282 let occurrence = self
6283 .identity_counts
6284 .entry(base_identity.clone())
6285 .or_default();
6286 let identity = if *occurrence == 0 {
6287 base_identity
6288 } else {
6289 format!("{base_identity}-{}", *occurrence + 1)
6290 };
6291 *occurrence += 1;
6292 self.sources.insert(
6293 span,
6294 ClosureSource {
6295 ordinal,
6296 binding: self.bindings.get(&span).cloned(),
6297 identity,
6298 },
6299 );
6300 visit::visit_expr_closure(self, node);
6301 }
6302
6303 fn visit_macro(&mut self, node: &'ast Macro) {
6304 let Some(expressions) = evaluated_macro_expressions(node) else {
6305 return;
6306 };
6307 for expression in &expressions {
6308 self.visit_expr(expression);
6309 }
6310 }
6311
6312 fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
6313}
6314
6315fn closure_structural_identity(node: &ExprClosure) -> String {
6316 let signature = format!(
6317 "{}:{}:{}:{}",
6318 node.asyncness.is_some(),
6319 node.movability.is_some(),
6320 node.capture.is_some(),
6321 node.inputs
6322 .iter()
6323 .map(compact_tokens)
6324 .collect::<Vec<_>>()
6325 .join(",")
6326 );
6327 let mut hasher = Sha256::new();
6328 hasher.update(signature.as_bytes());
6329 format!("{:x}", hasher.finalize())[..12].to_owned()
6330}
6331
6332struct ClosureLabelRedactor<'a> {
6333 closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
6334 used: Vec<(String, String)>,
6335}
6336
6337impl VisitMut for ClosureLabelRedactor<'_> {
6338 fn visit_expr_mut(&mut self, node: &mut Expr) {
6339 let Expr::Closure(closure) = node else {
6340 visit_mut::visit_expr_mut(self, node);
6341 return;
6342 };
6343 let Some(source) = self
6344 .closure_sources
6345 .get(&closure_span(closure.span()))
6346 .cloned()
6347 else {
6348 return;
6349 };
6350 let marker = format!("__diffkit_closure_placeholder_{}", source.ordinal);
6351 self.used.push((marker.clone(), source.lambda_name()));
6352 let ident = syn::Ident::new(&marker, closure.span());
6353 *node = Expr::Path(syn::ExprPath {
6354 attrs: Vec::new(),
6355 qself: None,
6356 path: syn::Path::from(ident),
6357 });
6358 }
6359}
6360
6361fn compact_expression(
6362 expression: &Expr,
6363 closure_sources: &HashMap<ClosureSpan, ClosureSource>,
6364) -> String {
6365 let mut expression = expression.clone();
6366 let mut redactor = ClosureLabelRedactor {
6367 closure_sources,
6368 used: Vec::new(),
6369 };
6370 redactor.visit_expr_mut(&mut expression);
6371 let mut rendered = compact_tokens(&expression);
6372 for (marker, lambda) in redactor.used {
6373 rendered = rendered.replace(&marker, &lambda);
6374 }
6375 rendered
6376}
6377
6378fn call_argument_label(
6379 expression: &Expr,
6380 closure_sources: &HashMap<ClosureSpan, ClosureSource>,
6381) -> String {
6382 let rendered = compact_expression(expression, closure_sources);
6383 if rendered.chars().count() <= 96 {
6384 rendered
6385 } else {
6386 "…".to_owned()
6387 }
6388}
6389
6390fn call_arguments(
6391 arguments: &syn::punctuated::Punctuated<Expr, syn::Token![,]>,
6392 closure_sources: &HashMap<ClosureSpan, ClosureSource>,
6393) -> (Vec<String>, Vec<String>) {
6394 let mut closure_types = Vec::new();
6395 let mut values = Vec::new();
6396 for argument in arguments {
6397 if let Some(source) = closure_source_for_expression(argument, closure_sources) {
6398 closure_types.push(source.lambda_name());
6399 } else {
6400 values.push(call_argument_label(argument, closure_sources));
6401 }
6402 }
6403 (closure_types, values)
6404}
6405
6406fn closure_source_for_expression<'a>(
6407 expression: &Expr,
6408 closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
6409) -> Option<&'a ClosureSource> {
6410 if let Some(closure) = direct_closure_expression(expression) {
6411 return closure_sources.get(&closure_span(closure.span()));
6412 }
6413 let Expr::Path(path) = expression else {
6414 return None;
6415 };
6416 let binding = path.path.get_ident()?.to_string();
6417 closure_sources
6418 .values()
6419 .find(|source| source.binding.as_deref() == Some(binding.as_str()))
6420}
6421
6422fn direct_closure_expression(expression: &Expr) -> Option<&ExprClosure> {
6423 match expression {
6424 Expr::Closure(closure) => Some(closure),
6425 Expr::Group(group) => direct_closure_expression(&group.expr),
6426 Expr::Paren(paren) => direct_closure_expression(&paren.expr),
6427 _ => None,
6428 }
6429}
6430
6431fn simple_pattern_binding(pattern: &Pat) -> Option<String> {
6432 match pattern {
6433 Pat::Ident(identifier) => Some(identifier.ident.to_string()),
6434 Pat::Type(typed) => simple_pattern_binding(&typed.pat),
6435 _ => None,
6436 }
6437}
6438
6439fn append_generic_arguments(callee: &str, additions: &[String]) -> String {
6440 if additions.is_empty() {
6441 return callee.to_owned();
6442 }
6443 let additions = additions.join(", ");
6444 let Some(start) = trailing_generic_arguments_start(callee) else {
6445 return format!("{callee}<{additions}>");
6446 };
6447 let existing = &callee[start + 1..callee.len() - 1];
6448 let separator = if existing.is_empty() { "" } else { ", " };
6449 format!("{}<{existing}{separator}{additions}>", &callee[..start])
6450}
6451
6452fn trailing_generic_arguments_start(value: &str) -> Option<usize> {
6453 if !value.ends_with('>') {
6454 return None;
6455 }
6456 let mut depth = 0usize;
6457 for (index, character) in value.char_indices().rev() {
6458 match character {
6459 '>' => depth += 1,
6460 '<' => {
6461 depth = depth.checked_sub(1)?;
6462 if depth == 0 {
6463 return Some(index);
6464 }
6465 }
6466 _ => {}
6467 }
6468 }
6469 None
6470}
6471
6472fn simple_receiver(expression: &Expr) -> bool {
6473 match expression {
6474 Expr::Path(_) => true,
6475 Expr::Field(field) => simple_receiver(&field.base),
6476 Expr::Index(index) => simple_receiver(&index.expr),
6477 Expr::Paren(paren) => simple_receiver(&paren.expr),
6478 Expr::Group(group) => simple_receiver(&group.expr),
6479 Expr::Reference(reference) => simple_receiver(&reference.expr),
6480 Expr::Unary(unary) => simple_receiver(&unary.expr),
6481 _ => false,
6482 }
6483}
6484
6485fn callable_path(expression: &Expr) -> Option<Vec<String>> {
6486 match expression {
6487 Expr::Path(path) => Some(
6488 path.path
6489 .segments
6490 .iter()
6491 .map(|segment| segment.ident.to_string())
6492 .collect(),
6493 ),
6494 Expr::Group(group) => callable_path(&group.expr),
6495 Expr::Paren(paren) => callable_path(&paren.expr),
6496 _ => None,
6497 }
6498}
6499
6500fn is_self_expr(expression: &Expr) -> bool {
6501 matches!(expression, Expr::Path(path) if path.path.is_ident("self"))
6502}
6503
6504fn receiver_name(expression: &Expr) -> String {
6505 match expression {
6506 Expr::Path(path) => path
6507 .path
6508 .segments
6509 .iter()
6510 .map(|segment| segment.ident.to_string())
6511 .collect::<Vec<_>>()
6512 .join("::"),
6513 _ => "<expr>".to_owned(),
6514 }
6515}
6516
6517#[cfg(test)]
6518mod tests {
6519 use std::fs;
6520 use std::path::Path;
6521
6522 use super::*;
6523
6524 #[test]
6525 fn extracts_functions_methods_calls_and_signature_facts() {
6526 let source = r#"
6527 pub trait Store {
6528 fn save(&mut self, value: Item) {
6529 validate(value);
6530 }
6531 }
6532
6533 struct Database;
6534
6535 impl Store for Database {
6536 fn save(&mut self, value: Item) {
6537 self.prepare();
6538 persist(value);
6539 }
6540 }
6541 "#;
6542 let frontend = RustBackend;
6543 let analysis = frontend
6544 .analyze_file(
6545 &FileContext {
6546 path: Path::new("src/lib.rs"),
6547 module: &[],
6548 },
6549 source,
6550 )
6551 .unwrap();
6552
6553 assert_eq!(analysis.functions.len(), 2);
6554 let implementation = analysis
6555 .functions
6556 .iter()
6557 .find(|function| function.id.container.as_deref() == Some("Database as Store"))
6558 .unwrap();
6559 assert_eq!(implementation.calls.len(), 2);
6560 assert!(analysis.facts.iter().any(|fact| {
6561 fact.subject == implementation.id
6562 && fact.kind == "receiver"
6563 && fact.value == "&mut self"
6564 }));
6565 assert!(analysis.facts.iter().any(|fact| {
6566 fact.subject == implementation.id && fact.kind == "impl-trait" && fact.key == "Store"
6567 }));
6568 }
6569
6570 #[test]
6571 fn closure_arguments_are_labels_not_serialized_bodies() {
6572 let source = r#"
6573 fn run(values: &[String]) {
6574 values
6575 .iter()
6576 .filter_map(|value| {
6577 parse(value);
6578 Some(value)
6579 })
6580 .collect::<Vec<_>>();
6581 }
6582 fn parse(_: &String) {}
6583 "#;
6584 let frontend = RustBackend;
6585 let analysis = frontend
6586 .analyze_file(
6587 &FileContext {
6588 path: Path::new("src/lib.rs"),
6589 module: &[],
6590 },
6591 source,
6592 )
6593 .unwrap();
6594
6595 let run = analysis
6596 .functions
6597 .iter()
6598 .find(|function| function.label.default.starts_with("run("))
6599 .unwrap();
6600 let labels = run
6601 .calls
6602 .iter()
6603 .map(|call| call.label.default.as_str())
6604 .collect::<Vec<_>>();
6605
6606 assert_eq!(
6607 labels,
6608 ["values.iter()", "filter_map<λ#1>()", "collect<Vec<_>>()",]
6609 );
6610 assert!(labels.iter().all(|label| !label.contains("parse(value)")));
6611
6612 let closure = analysis
6613 .functions
6614 .iter()
6615 .find(|function| function.label.default.starts_with("λ#1("))
6616 .unwrap();
6617 assert!(
6618 closure
6619 .calls
6620 .iter()
6621 .any(|call| call.label.default == "parse(value)")
6622 );
6623 }
6624
6625 #[test]
6626 fn rustc_public_can_analyze_a_compilable_file() {
6627 let directory =
6628 std::env::temp_dir().join(format!("diffkit-rustc-public-{}", std::process::id()));
6629 fs::create_dir_all(&directory).unwrap();
6630 let path = directory.join("input.rs");
6631 fs::write(&path, "pub fn entry() { helper(); } fn helper() {}\n").unwrap();
6632
6633 let analysis = analyze_semantic_file(&path).unwrap();
6634
6635 assert!(
6636 analysis
6637 .functions
6638 .iter()
6639 .any(|function| function.id.name.ends_with("::entry"))
6640 );
6641 assert!(
6642 analysis
6643 .functions
6644 .iter()
6645 .any(|function| function.id.name.ends_with("::helper"))
6646 );
6647 fs::remove_dir_all(directory).unwrap();
6648 }
6649
6650 #[test]
6651 fn rustc_public_skips_higher_ranked_vtables() {
6652 let directory = std::env::temp_dir().join(format!(
6653 "diffkit-rustc-public-bound-vtable-{}",
6654 std::process::id()
6655 ));
6656 fs::create_dir_all(&directory).unwrap();
6657 let path = directory.join("input.rs");
6658 fs::write(
6659 &path,
6660 r#"
6661 use std::{
6662 any::{Any, TypeId},
6663 collections::HashMap,
6664 sync::Arc,
6665 };
6666
6667 pub fn populate() {
6668 let mut values: HashMap<TypeId, Arc<dyn Any + Send + Sync>> = HashMap::new();
6669 values.insert(TypeId::of::<u8>(), Arc::new(1_u8));
6670 values.reserve(1);
6671 }
6672 "#,
6673 )
6674 .unwrap();
6675
6676 let analysis = analyze_semantic_file(&path).unwrap();
6677
6678 assert!(
6679 analysis
6680 .functions
6681 .iter()
6682 .any(|function| function.label.default.starts_with("populate("))
6683 );
6684 fs::remove_dir_all(directory).unwrap();
6685 }
6686
6687 #[test]
6688 fn rustc_public_does_not_open_opaque_instance_arguments() {
6689 let directory = std::env::temp_dir().join(format!(
6690 "diffkit-rustc-public-opaque-argument-{}",
6691 std::process::id()
6692 ));
6693 fs::create_dir_all(&directory).unwrap();
6694 let path = directory.join("input.rs");
6695 fs::write(
6696 &path,
6697 r#"
6698 fn make() -> impl Copy { 1_u8 }
6699 fn consume<T: Copy>(_value: T) {}
6700 pub fn run() { consume(make()); }
6701 "#,
6702 )
6703 .unwrap();
6704
6705 let analysis = analyze_semantic_file(&path).unwrap();
6706
6707 assert!(
6708 analysis
6709 .functions
6710 .iter()
6711 .any(|function| function.label.default.starts_with("run("))
6712 );
6713 fs::remove_dir_all(directory).unwrap();
6714 }
6715
6716 #[test]
6717 fn semantic_analysis_omits_enum_variant_constructors() {
6718 let directory = std::env::temp_dir().join(format!(
6719 "diffkit-rustc-public-constructors-{}",
6720 std::process::id()
6721 ));
6722 fs::create_dir_all(&directory).unwrap();
6723 let path = directory.join("input.rs");
6724 let source = r#"
6725 pub struct Node;
6726 pub fn root(before: Option<&Node>, after: Option<&Node>) -> Option<()> {
6727 match (before, after) {
6728 (Some(before), Some(after)) => Some(inner(Some(before), Some(after))),
6729 _ => None,
6730 }
6731 }
6732 fn inner(_: Option<&Node>, _: Option<&Node>) {}
6733 pub fn is_json(value: Option<&str>) -> bool { value == Some("json") }
6734 "#;
6735 fs::write(&path, source).unwrap();
6736
6737 let analysis = analyze_semantic_file(&path).unwrap();
6738 let root = analysis
6739 .functions
6740 .iter()
6741 .find(|function| function.label.default.starts_with("root("))
6742 .unwrap();
6743 let calls = root
6744 .calls
6745 .iter()
6746 .map(|call| call.label.default.as_str())
6747 .collect::<Vec<_>>();
6748
6749 assert_eq!(calls, ["inner(Some(before), Some(after))"]);
6750 let is_json = analysis
6751 .functions
6752 .iter()
6753 .find(|function| function.label.default.starts_with("is_json("))
6754 .unwrap();
6755 assert!(is_json.calls.is_empty(), "{:#?}", is_json.calls);
6756 fs::remove_dir_all(directory).unwrap();
6757 }
6758
6759 #[test]
6760 fn semantic_labels_do_not_expose_external_inferred_generics_or_closure_bodies() {
6761 let directory = std::env::temp_dir().join(format!(
6762 "diffkit-rustc-public-call-labels-{}",
6763 std::process::id()
6764 ));
6765 fs::create_dir_all(&directory).unwrap();
6766 let path = directory.join("input.rs");
6767 fs::write(
6768 &path,
6769 r#"
6770 pub fn run(values: &[String]) -> Vec<usize> {
6771 values
6772 .iter()
6773 .filter_map(|value| value.parse::<usize>().ok())
6774 .collect()
6775 }
6776 "#,
6777 )
6778 .unwrap();
6779
6780 let analysis = analyze_semantic_file(&path).unwrap();
6781 let run = analysis
6782 .functions
6783 .iter()
6784 .find(|function| function.label.default.starts_with("run("))
6785 .unwrap();
6786 let labels = run
6787 .calls
6788 .iter()
6789 .map(|call| call.label.default.as_str())
6790 .collect::<Vec<_>>();
6791
6792 assert_eq!(labels, ["values.iter()", "filter_map<λ#1>()", "collect()"]);
6793 assert!(
6794 labels
6795 .iter()
6796 .all(|label| !label.contains("impl ") && !label.contains("|value|"))
6797 );
6798 fs::remove_dir_all(directory).unwrap();
6799 }
6800
6801 #[test]
6802 fn rustc_public_resolves_generic_trait_dispatch() {
6803 let directory = std::env::temp_dir().join(format!(
6804 "diffkit-rustc-public-generics-{}",
6805 std::process::id()
6806 ));
6807 fs::create_dir_all(&directory).unwrap();
6808 let path = directory.join("input.rs");
6809 fs::write(
6810 &path,
6811 r#"
6812 #[derive(Clone, Copy)]
6813 pub struct Order;
6814 trait Store { fn save(&self, order: Order); }
6815 struct Postgres;
6816 impl Store for Postgres {
6817 fn save(&self, order: Order) {
6818 sql::begin();
6819 sql::insert(order);
6820 sql::commit();
6821 }
6822 }
6823 struct S3;
6824 impl Store for S3 {
6825 fn save(&self, order: Order) {
6826 aws::sign(order);
6827 aws::put_object(order);
6828 }
6829 }
6830 fn run<S: Store>(storage: &S, order: Order) {
6831 validate(order);
6832 storage.save(order);
6833 finalize(order);
6834 }
6835 pub fn entry(order: Order) {
6836 run(&Postgres, order);
6837 run(&S3, order);
6838 }
6839 fn validate(_: Order) {}
6840 fn finalize(_: Order) {}
6841 mod sql {
6842 use super::Order;
6843 pub fn begin() {}
6844 pub fn insert(_: Order) {}
6845 pub fn commit() {}
6846 }
6847 mod aws {
6848 use super::Order;
6849 pub fn sign(_: Order) {}
6850 pub fn put_object(_: Order) {}
6851 }
6852 "#,
6853 )
6854 .unwrap();
6855
6856 let analysis = analyze_semantic_file(&path).unwrap();
6857 let labels = analysis
6858 .functions
6859 .iter()
6860 .map(|function| {
6861 (
6862 function.label.default.clone(),
6863 function
6864 .calls
6865 .iter()
6866 .map(|call| call.label.default.clone())
6867 .collect::<Vec<_>>(),
6868 )
6869 })
6870 .collect::<Vec<_>>();
6871 assert!(labels.iter().any(|(label, calls)| {
6872 label == "run<Postgres>(storage, order)"
6873 && calls.iter().any(|call| call == "Postgres::save(order)")
6874 }));
6875 assert!(labels.iter().any(|(label, calls)| {
6876 label == "run<S3>(storage, order)" && calls.iter().any(|call| call == "S3::save(order)")
6877 }));
6878 fs::remove_dir_all(directory).unwrap();
6879 }
6880
6881 #[test]
6882 fn specialization_consumes_duplicate_normalized_instances() {
6883 let span = SourceSpan {
6884 file: PathBuf::from("src/lib.rs"),
6885 start_line: 1,
6886 start_column: 1,
6887 start_byte: None,
6888 end_line: 1,
6889 end_column: 1,
6890 end_byte: None,
6891 };
6892 let function = || RawSemanticFunction {
6893 key: "crate::duplicate".to_owned(),
6894 display: "duplicate".to_owned(),
6895 definition_name: "crate::duplicate".to_owned(),
6896 body_span: span.clone(),
6897 calls: Vec::new(),
6898 constructor_spans: Vec::new(),
6899 constructor_names: Vec::new(),
6900 parameter_types: Vec::new(),
6901 };
6902
6903 let specialized = specialize_semantic_functions(
6904 &[function(), function()],
6905 &[],
6906 &mut DispatchResolver::default(),
6907 "crate",
6908 );
6909
6910 assert_eq!(specialized.len(), 1);
6911 assert_eq!(specialized[0].key, "crate::duplicate");
6912 }
6913
6914 #[test]
6915 fn closure_locations_survive_until_the_source_lambda_mapping() {
6916 assert_eq!(
6917 normalize_instance_key("core::result::Result::map::<{closure@src/lib.rs:10:4: 10:12}>"),
6918 "core::result::Result::map<{closure@src/lib.rs:10:4: 10:12}>"
6919 );
6920 assert_eq!(
6921 normalize_type_display(
6922 "diffkit::Runner<{async block@src/lib.rs:20:8: 24:9}>",
6923 "diffkit"
6924 ),
6925 "Runner<{async block}>"
6926 );
6927 }
6928}