Skip to main content

diffkit/language/
rust.rs

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
57/// Analyze a standalone, compilable Rust source file with rustc's typed MIR.
58///
59/// Analyze Rust by combining source-shaped labels and spans with rustc's typed
60/// MIR. The source extractor is an internal stage, not a separate analysis
61/// mode.
62pub 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
119/// Whether this process was started by Cargo as DiffKit's rustc workspace
120/// wrapper. The binary checks this before parsing user-facing CLI arguments.
121pub fn rustc_wrapper_requested() -> bool {
122    env::var_os(RUSTC_CAPTURE_DIRECTORY).is_some()
123}
124
125/// Run one rustc invocation and persist its semantic result for the parent
126/// DiffKit process. Dependency crates are not intercepted because the parent
127/// uses `RUSTC_WORKSPACE_WRAPPER`, not the broader `RUSTC_WRAPPER`.
128pub 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
185/// Analyze every Rust target selected by Cargo under `project_root` using the
186/// exact rustc invocations Cargo produced. The returned graph fragment is the
187/// only representation that crosses into DiffKit's common engine.
188pub(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    // `cargo clean --workspace` and the following check form one operation.
232    // Cargo's own target lock covers each command separately, so another
233    // DiffKit process could otherwise clean between them and then observe a
234    // fresh build without receiving any semantic wrapper output.
235    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        // Before and after snapshots have different source roots. Reusing
283        // rustc incremental state across those roots can mix stale module
284        // paths even though Cargo correctly reruns the workspace target.
285        .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    // Engine and renderer rebuilds do not change captured rustc facts. Cache
430    // compatibility is owned explicitly by the semantic schema so a local
431    // binary timestamp cannot invalidate every analyzed project.
432    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
564/// Owns one Cargo target directory for one semantic-analysis endpoint.
565///
566/// A before/after comparison must not share a persistent target directory:
567/// Cargo identifies workspace packages independently of the temporary Git
568/// snapshot path, so a binary from one endpoint can otherwise be checked
569/// against stale library metadata from the other endpoint.
570pub(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    // Return-value summaries remain symbolic in terms of the callee's formal
1324    // parameters. This lets a trait object cross ordinary helper functions
1325    // without losing its concrete provenance. The lattice is finite, so the
1326    // monotone iteration also handles recursive helpers.
1327    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            // `TraitRef` cannot represent the binder around a higher-ranked
1863            // principal such as `dyn for<'a> Fn(&'a T)`. Flattening it would
1864            // pass escaping bound variables to rustc's vtable query and panic.
1865            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        // A trait upcast (`dyn Child` -> `dyn Parent`) does not reveal a
1886        // concrete implementation. Only thin-to-wide coercions contribute an
1887        // concrete receiver-flow candidate here.
1888        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    /// Aliases inferred across an opaque external boundary. Reads can retain
2061    /// the known incoming candidates, but a write through one cannot soundly
2062    /// be attributed to a particular source place.
2063    uncertain_aliases: HashSet<Place>,
2064    constant_indices: HashMap<usize, u64>,
2065    /// Places initialized by MIR even when their exact dynamic candidate set
2066    /// is empty (for example `Vec::new()`). Absence from `flows` alone cannot
2067    /// distinguish known-empty state from opaque memory.
2068    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
2386/// Apply a callee's symbolic writes to the caller's MIR places. Effects are
2387/// expressed in terms of formal parameters, so this works for standard and
2388/// user-defined containers without method-name contracts. Only a call whose
2389/// body was unavailable falls back to an opaque boundary.
2390struct 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        // The fixed point has not reached this analyzed callee yet. Adding an
2430        // opaque reason here would be irreversible in the monotone lattice and
2431        // would leave a false `[partial]` after its exact summary arrives.
2432        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 &region.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(&region), &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    // Asking rustc_public for `TyKind` still panics on some valid coroutine
2752    // closure and alias shapes. Its bounded display is safe for this shallow
2753    // containment predicate; deep constructor traversal is guarded below.
2754    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    // rustc_public cannot currently lower `CoroutineClosure` to its public
2776    // `TyKind`. The unsupported value may also sit inside a tuple or ADT, in
2777    // which case opening the outer kind recursively triggers the same panic.
2778    // Constructor names are only a source-call suppression aid, so skipping
2779    // this compiler-generated environment is both safe and sufficient.
2780    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        // Function and coroutine generic arguments encode their environment
2798        // and can duplicate captured type trees exponentially. Constructors
2799        // used in their bodies are present in those bodies' own MIR locals;
2800        // walking the environment here adds no source-call information.
2801        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            // Any opacity of a whole-value region is already carried in the
2861            // flow itself. Adding a fresh reason merely because a known local
2862            // aggregate is projected turns exact container provenance into a
2863            // false `[partial]` result.
2864            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            // Multiple compiler instances can normalize to the same display
3227            // key. Even when the emitted semantic function was already seen,
3228            // this raw instance is consumed; otherwise the disconnected-root
3229            // fallback below selects it forever.
3230            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(&parameter))
3407            || matches!(
3408                &call.target,
3409                RawSemanticCallTarget::Dynamic { receiver, .. }
3410                    if receiver.parameters.contains(&parameter)
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(&parameter))
3699            || matches!(
3700                &call.target,
3701                SemanticCallTarget::Dynamic { receiver_flow, .. }
3702                    if receiver_flow.parameters.contains(&parameter)
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            // Compiler-generated functions (drop glue, coroutine shims, etc.)
3881            // are intentionally omitted until they have explicit source syntax.
3882            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    // An async fn is represented by a compiler coroutine body whose source
4115    // span belongs to the function itself. Other compiler closures must have
4116    // an explicit source closure template; otherwise macro-generated bodies
4117    // would be misattributed to the enclosing function as duplicate roots.
4118    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
4850/// rustc's display form intentionally prints a closure type using only its
4851/// definition location. Distinct monomorphizations of the same closure can
4852/// therefore have identical names even though their captured generic
4853/// environments (and call trees) differ. Preserve a compact structural
4854/// environment in semantic identities, while the display layer strips it.
4855///
4856/// Only direct ordinary closures are opened. Looking through arbitrary
4857/// `TyKind`s is unsafe with the current rustc_public API because conversion of
4858/// coroutine-closure and some alias types is still unimplemented. Direct
4859/// closure arguments are the substitution chain that distinguishes the
4860/// monomorphizations; aggregate captures are represented separately by their
4861/// normalized rendered shape.
4862fn 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    // `rustc_public` still has unimplemented conversions for aliases and
4938    // coroutine closures. Inspecting every nested `TyKind` can therefore panic
4939    // on otherwise valid crates (Rayon contains such aliases). Ordinary
4940    // closures passed as function generics are direct arguments here, so only
4941    // open that known-safe shape.
4942    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        // rustc exposes the generated future returned by an async closure as
5126        // an extra monomorphization argument. The source closure is the
5127        // callable identity users wrote; keep that compact source generic and
5128        // hide the compiler-only `{async closure body}` type.
5129        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(&parameter.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(&parameter.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(&parameter.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        // MIR evaluates nested calls before their enclosing expression. Keep
6101        // the same order so an aggregate constructor such as `Some(f())`
6102        // cannot claim `f`'s semantic call span.
6103        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    // Nested closures and local functions own their calls; do not attribute them
6146    // to the enclosing function.
6147    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}