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::Span;
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, ExistentialTraitRef, GenericArgKind, GenericArgs, RigidTy, TraitRef,
21    Ty, TyKind, VtblEntry,
22};
23use serde::{Deserialize, Serialize};
24use sha2::{Digest, Sha256};
25use syn::spanned::Spanned;
26use syn::visit::{self, Visit};
27use syn::visit_mut::{self, VisitMut};
28use syn::{
29    Expr, ExprCall, ExprClosure, ExprMethodCall, FnArg, ImplItem, Item, ItemFn, ItemImpl,
30    ItemTrait, Pat, ReturnType, Signature, TraitItem, Visibility,
31};
32
33use super::{FileContext, FrontendResult, LanguageBackend, ProjectContext};
34use crate::model::{
35    CallLabel, CallSite, CallSiteId, CallSyntax, CallTarget, DispatchCandidate, DispatchEvidence,
36    DispatchResolution, FileAnalysis, FunctionInfo, LanguageFact, LanguageId, SourceSpan, SymbolId,
37    UnresolvedReason,
38};
39
40#[derive(Default)]
41pub struct RustBackend;
42
43pub static RUST_BACKEND: RustBackend = RustBackend;
44
45static RUSTC_DRIVER_LOCK: Mutex<()> = Mutex::new(());
46static TEMP_SOURCE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
47const RUSTC_CAPTURE_DIRECTORY: &str = "DIFFKIT_RUSTC_CAPTURE_DIRECTORY";
48const RUSTC_ANALYSIS_ROOT: &str = "DIFFKIT_RUSTC_ANALYSIS_ROOT";
49const SNAPSHOT_FINGERPRINT_FILE: &str = ".diffkit-snapshot-key";
50const RUST_SEMANTIC_CACHE_SCHEMA: u32 = 4;
51
52/// Analyze a standalone, compilable Rust source file with rustc's typed MIR.
53///
54/// Analyze Rust by combining source-shaped labels and spans with rustc's typed
55/// MIR. The source extractor is an internal stage, not a separate analysis
56/// mode.
57pub fn analyze_semantic_file(path: &Path) -> FrontendResult<FileAnalysis> {
58    analyze_semantic_file_with_entries(path, &[])
59}
60
61pub fn analyze_semantic_file_with_entries(
62    path: &Path,
63    entries: &[String],
64) -> FrontendResult<FileAnalysis> {
65    if !path.is_file() {
66        return Err(std::io::Error::other(format!(
67            "Rust analysis currently requires a standalone .rs file: {}",
68            path.display()
69        ))
70        .into());
71    }
72
73    let source = fs::read_to_string(path)?;
74    let syntax = RustBackend.analyze_file(&FileContext { path, module: &[] }, &source)?;
75    let semantic = collect_rustc_program(path)?;
76    let analysis = merge_semantic_program(syntax.clone(), semantic);
77    let missing_entries = entries
78        .iter()
79        .filter(|entry| entry.contains('<') && !analysis_has_entry(&analysis, entry))
80        .cloned()
81        .collect::<Vec<_>>();
82    if missing_entries.is_empty() {
83        return Ok(analysis);
84    }
85
86    let Some(seeded_source) = append_entry_seeds(&source, &missing_entries)? else {
87        return Ok(analysis);
88    };
89    let temporary = TemporaryRustSource::create(&seeded_source)?;
90    let temporary_path = temporary.path();
91    let mut semantic = collect_rustc_program(&temporary_path)?;
92    remap_semantic_file(&mut semantic, &temporary_path, path);
93    Ok(merge_semantic_program(syntax, semantic))
94}
95
96fn remap_semantic_file(program: &mut SemanticProgram, from: &Path, to: &Path) {
97    for function in &mut program.functions {
98        if source_files_match(&function.body_span.file, from) {
99            function.body_span.file = to.to_path_buf();
100        }
101        for call in &mut function.calls {
102            if source_files_match(&call.span.file, from) {
103                call.span.file = to.to_path_buf();
104            }
105        }
106    }
107}
108
109pub fn analyze_semantic_source(source: &str, entries: &[String]) -> FrontendResult<FileAnalysis> {
110    let temporary = TemporaryRustSource::create(source)?;
111    analyze_semantic_file_with_entries(&temporary.path(), entries)
112}
113
114/// Whether this process was started by Cargo as DiffKit's rustc workspace
115/// wrapper. The binary checks this before parsing user-facing CLI arguments.
116pub fn rustc_wrapper_requested() -> bool {
117    env::var_os(RUSTC_CAPTURE_DIRECTORY).is_some()
118}
119
120/// Run one rustc invocation and persist its semantic result for the parent
121/// DiffKit process. Dependency crates are not intercepted because the parent
122/// uses `RUSTC_WORKSPACE_WRAPPER`, not the broader `RUSTC_WRAPPER`.
123pub fn run_rustc_wrapper() -> FrontendResult<()> {
124    let mut arguments = env::args_os().skip(1);
125    let rustc = arguments
126        .next()
127        .ok_or_else(|| std::io::Error::other("rustc wrapper did not receive a compiler path"))?;
128    let arguments = arguments.collect::<Vec<_>>();
129    let mut driver_arguments = vec![rustc.to_string_lossy().into_owned()];
130    driver_arguments.extend(
131        arguments
132            .iter()
133            .cloned()
134            .map(|argument| {
135                argument
136                    .into_string()
137                    .map_err(|_| std::io::Error::other("rustc argument is not valid UTF-8"))
138            })
139            .collect::<Result<Vec<_>, _>>()?,
140    );
141
142    let program = match rustc_public::run!(&driver_arguments, || {
143        std::ops::ControlFlow::<(), SemanticProgram>::Continue(collect_instances())
144    }) {
145        Ok(program) => program,
146        Err(CompilerError::Failed) => {
147            return Err(std::io::Error::other("rustc failed during semantic capture").into());
148        }
149        Err(CompilerError::Skipped) => return Ok(()),
150        Err(CompilerError::Interrupted(())) => {
151            return Err(std::io::Error::other("rustc semantic capture was interrupted").into());
152        }
153    };
154
155    persist_semantic_program(&program)
156}
157
158fn persist_semantic_program(program: &SemanticProgram) -> FrontendResult<()> {
159    let directory = PathBuf::from(env::var_os(RUSTC_CAPTURE_DIRECTORY).ok_or_else(|| {
160        std::io::Error::other("semantic capture directory disappeared from the environment")
161    })?);
162    fs::create_dir_all(&directory)?;
163    let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
164    let destination = directory.join(format!("{}-{sequence}.json", std::process::id()));
165    let temporary = destination.with_extension("json.tmp");
166    fs::write(&temporary, serde_json::to_vec(program)?)?;
167    fs::rename(temporary, destination)?;
168    Ok(())
169}
170
171/// Analyze every Rust target selected by Cargo under `project_root` using the
172/// exact rustc invocations Cargo produced. The returned graph fragment is the
173/// only representation that crosses into DiffKit's common engine.
174pub(crate) fn analyze_semantic_project(
175    project_root: &Path,
176    wrapper_executable: &Path,
177    entries: &[String],
178    session: &RustProjectSession,
179) -> FrontendResult<FileAnalysis> {
180    let root = project_root.canonicalize()?;
181    if !root.join("Cargo.toml").is_file() {
182        return Err(std::io::Error::other(format!(
183            "no Cargo.toml at Rust project root {}",
184            root.display()
185        ))
186        .into());
187    }
188    let generic_entries = entries
189        .iter()
190        .filter(|entry| entry.contains('<'))
191        .cloned()
192        .collect::<Vec<_>>();
193    if generic_entries.is_empty() {
194        return capture_semantic_project(&root, wrapper_executable, session);
195    }
196
197    let seeded = SeededRustProject::create(&root)?;
198    seed_project_entries(seeded.root(), &generic_entries)?;
199    let mut analysis = capture_semantic_project(seeded.root(), wrapper_executable, session)?;
200    remap_analysis_root(&mut analysis, seeded.root(), &root);
201    remove_seed_functions(&mut analysis);
202    Ok(analysis)
203}
204
205fn capture_semantic_project(
206    root: &Path,
207    wrapper_executable: &Path,
208    session: &RustProjectSession,
209) -> FrontendResult<FileAnalysis> {
210    let started = std::time::Instant::now();
211    let fingerprint_started = std::time::Instant::now();
212    let fingerprint = session
213        .cache_path()
214        .map(|_| semantic_cache_fingerprint(root))
215        .transpose()?;
216    let fingerprint_elapsed = fingerprint_started.elapsed();
217    // `cargo clean --workspace` and the following check form one operation.
218    // Cargo's own target lock covers each command separately, so another
219    // DiffKit process could otherwise clean between them and then observe a
220    // fresh build without receiving any semantic wrapper output.
221    let cache_lock = fs::OpenOptions::new()
222        .create(true)
223        .read(true)
224        .write(true)
225        .truncate(false)
226        .open(session.target().join(".diffkit.lock"))?;
227    cache_lock.lock()?;
228    if let (Some(cache_path), Some(fingerprint)) = (session.cache_path(), fingerprint.as_deref())
229        && let Some(analysis) = read_analysis_cache(cache_path, fingerprint, root)
230    {
231        session.report_cache(true, fingerprint, fingerprint_elapsed, started.elapsed());
232        return Ok(analysis);
233    }
234    let capture = RustProjectCapture::create()?;
235    let wrapper = wrapper_executable.canonicalize()?;
236    let clean = Command::new("cargo")
237        .args(["clean", "--workspace", "--quiet"])
238        .current_dir(root)
239        .env("CARGO_TARGET_DIR", session.target())
240        .output()?;
241    if !clean.status.success() {
242        let diagnostic = String::from_utf8_lossy(&clean.stderr).trim().to_owned();
243        return Err(std::io::Error::other(if diagnostic.is_empty() {
244            format!("Cargo cache preparation exited with {}", clean.status)
245        } else {
246            diagnostic
247        })
248        .into());
249    }
250    let output = Command::new("cargo")
251        .args([
252            "check",
253            "--workspace",
254            "--all-targets",
255            "--all-features",
256            "--quiet",
257        ])
258        .current_dir(root)
259        .env("RUSTC_WORKSPACE_WRAPPER", &wrapper)
260        .env(RUSTC_CAPTURE_DIRECTORY, capture.results())
261        .env(RUSTC_ANALYSIS_ROOT, root)
262        .env("CARGO_TARGET_DIR", session.target())
263        // Before and after snapshots have different source roots. Reusing
264        // rustc incremental state across those roots can mix stale module
265        // paths even though Cargo correctly reruns the workspace target.
266        .env("CARGO_INCREMENTAL", "0")
267        .output()?;
268    if !output.status.success() {
269        let diagnostic = String::from_utf8_lossy(&output.stderr).trim().to_owned();
270        return Err(std::io::Error::other(if diagnostic.is_empty() {
271            format!("Cargo semantic analysis exited with {}", output.status)
272        } else {
273            diagnostic
274        })
275        .into());
276    }
277
278    let mut result_files = fs::read_dir(capture.results())?.collect::<Result<Vec<_>, _>>()?;
279    result_files.sort_by_key(std::fs::DirEntry::path);
280    let mut semantic = SemanticProgram {
281        crate_name: String::new(),
282        functions: Vec::new(),
283    };
284    for result_file in result_files {
285        if result_file
286            .path()
287            .extension()
288            .and_then(|value| value.to_str())
289            != Some("json")
290        {
291            continue;
292        }
293        let mut program: SemanticProgram = serde_json::from_slice(&fs::read(result_file.path())?)?;
294        semantic.functions.append(&mut program.functions);
295    }
296    if semantic.functions.is_empty() {
297        return Err(std::io::Error::other(format!(
298            "Cargo produced no Rust semantic targets under {}",
299            root.display()
300        ))
301        .into());
302    }
303    semantic.functions.sort_by(|left, right| {
304        (&left.key, &left.body_span.file, left.body_span.start_line).cmp(&(
305            &right.key,
306            &right.body_span.file,
307            right.body_span.start_line,
308        ))
309    });
310    semantic.functions.dedup_by(|left, right| {
311        left.key == right.key
312            && left.body_span.file == right.body_span.file
313            && left.body_span.start_line == right.body_span.start_line
314            && left.body_span.start_column == right.body_span.start_column
315    });
316    respecialize_merged_semantic_program(&mut semantic);
317
318    let syntax = analyze_project_sources(root)?;
319    let analysis = deduplicate_analysis(merge_semantic_program(syntax, semantic));
320    if let (Some(cache_path), Some(fingerprint)) = (session.cache_path(), fingerprint) {
321        let cache = CachedRustAnalysis {
322            schema: RUST_SEMANTIC_CACHE_SCHEMA,
323            fingerprint,
324            root: root.to_path_buf(),
325            analysis,
326        };
327        let _ = write_analysis_cache(cache_path, &cache);
328        session.report_cache(
329            false,
330            &cache.fingerprint,
331            fingerprint_elapsed,
332            started.elapsed(),
333        );
334        return Ok(cache.analysis);
335    }
336    Ok(analysis)
337}
338
339fn semantic_cache_fingerprint(root: &Path) -> std::io::Result<String> {
340    let mut hasher = Sha256::new();
341    hash_cache_bytes(&mut hasher, &RUST_SEMANTIC_CACHE_SCHEMA.to_le_bytes());
342    hash_cache_bytes(&mut hasher, &analyzer_identity());
343    hash_command_identity(&mut hasher, root, "rustc", &["-vV"])?;
344    hash_command_identity(&mut hasher, root, "cargo", &["-V"])?;
345
346    let mut environment = env::vars_os()
347        .filter(|(key, _)| rust_build_environment_key(key))
348        .collect::<Vec<_>>();
349    environment.sort_by(|left, right| {
350        left.0
351            .as_encoded_bytes()
352            .cmp(right.0.as_encoded_bytes())
353            .then_with(|| left.1.as_encoded_bytes().cmp(right.1.as_encoded_bytes()))
354    });
355    for (key, value) in environment {
356        hash_cache_bytes(&mut hasher, key.as_encoded_bytes());
357        hash_cache_bytes(&mut hasher, value.as_encoded_bytes());
358    }
359    if let Ok(snapshot_key) = fs::read(root.join(SNAPSHOT_FINGERPRINT_FILE)) {
360        hash_cache_bytes(&mut hasher, b"git-snapshot");
361        hash_cache_bytes(&mut hasher, &snapshot_key);
362    } else {
363        hash_project_inputs(root, root, &mut hasher)?;
364    }
365    Ok(format!("{:x}", hasher.finalize()))
366}
367
368fn rust_build_environment_key(key: &std::ffi::OsStr) -> bool {
369    let Some(key) = key.to_str() else {
370        return true;
371    };
372    matches!(
373        key,
374        "PATH"
375            | "HOME"
376            | "RUSTFLAGS"
377            | "RUSTDOCFLAGS"
378            | "RUSTC"
379            | "RUSTUP_HOME"
380            | "RUSTUP_TOOLCHAIN"
381            | "CARGO_HOME"
382            | "CARGO_BUILD_TARGET"
383            | "CARGO_ENCODED_RUSTFLAGS"
384            | "CC"
385            | "CXX"
386            | "AR"
387            | "CFLAGS"
388            | "CXXFLAGS"
389            | "LDFLAGS"
390            | "MACOSX_DEPLOYMENT_TARGET"
391            | "SDKROOT"
392            | "PKG_CONFIG_PATH"
393    ) || [
394        "CARGO_PROFILE_",
395        "CARGO_TARGET_",
396        "RUSTC_",
397        "CC_",
398        "CXX_",
399        "AR_",
400        "CFLAGS_",
401        "CXXFLAGS_",
402        "PKG_CONFIG_",
403    ]
404    .iter()
405    .any(|prefix| key.starts_with(prefix))
406}
407
408fn analyzer_identity() -> Vec<u8> {
409    // Engine and renderer rebuilds do not change captured rustc facts. Cache
410    // compatibility is owned explicitly by the semantic schema so a local
411    // binary timestamp cannot invalidate every analyzed project.
412    format!(
413        "{}:semantic-schema-{RUST_SEMANTIC_CACHE_SCHEMA}",
414        env!("CARGO_PKG_VERSION")
415    )
416    .into_bytes()
417}
418
419fn hash_command_identity(
420    hasher: &mut Sha256,
421    root: &Path,
422    program: &str,
423    arguments: &[&str],
424) -> std::io::Result<()> {
425    let output = Command::new(program)
426        .args(arguments)
427        .current_dir(root)
428        .output()?;
429    hash_cache_bytes(hasher, program.as_bytes());
430    hash_cache_bytes(hasher, &output.stdout);
431    hash_cache_bytes(hasher, &output.stderr);
432    hash_cache_bytes(hasher, &output.status.code().unwrap_or(-1).to_le_bytes());
433    Ok(())
434}
435
436fn hash_project_inputs(root: &Path, directory: &Path, hasher: &mut Sha256) -> std::io::Result<()> {
437    let mut entries = fs::read_dir(directory)?.collect::<Result<Vec<_>, _>>()?;
438    entries.sort_by_key(std::fs::DirEntry::path);
439    for entry in entries {
440        let path = entry.path();
441        let file_type = entry.file_type()?;
442        if file_type.is_dir() {
443            if !is_generated_directory(&path) {
444                hash_project_inputs(root, &path, hasher)?;
445            }
446            continue;
447        }
448
449        let relative = path.strip_prefix(root).unwrap_or(&path);
450        if file_type.is_file() {
451            hash_cache_bytes(hasher, b"file");
452            hash_cache_bytes(hasher, relative.as_os_str().as_encoded_bytes());
453            hash_file_contents(&path, hasher)?;
454        } else if file_type.is_symlink() {
455            hash_cache_bytes(hasher, b"symlink");
456            hash_cache_bytes(hasher, relative.as_os_str().as_encoded_bytes());
457            let target = fs::read_link(&path)?;
458            hash_cache_bytes(hasher, target.as_os_str().as_encoded_bytes());
459            if let Ok(resolved) = path.canonicalize()
460                && resolved.is_file()
461            {
462                hash_file_contents(&resolved, hasher)?;
463            }
464        }
465    }
466    Ok(())
467}
468
469fn is_generated_directory(path: &Path) -> bool {
470    matches!(
471        path.file_name().and_then(|name| name.to_str()),
472        Some(".git" | "target" | "node_modules" | ".zig-cache" | "zig-out" | "_build")
473    )
474}
475
476fn hash_file_contents(path: &Path, hasher: &mut Sha256) -> std::io::Result<()> {
477    let mut file = fs::File::open(path)?;
478    let length = file.metadata()?.len();
479    hasher.update(length.to_le_bytes());
480    let mut buffer = [0u8; 64 * 1024];
481    loop {
482        let read = file.read(&mut buffer)?;
483        if read == 0 {
484            break;
485        }
486        hasher.update(&buffer[..read]);
487    }
488    Ok(())
489}
490
491fn hash_cache_bytes(hasher: &mut Sha256, bytes: &[u8]) {
492    hasher.update((bytes.len() as u64).to_le_bytes());
493    hasher.update(bytes);
494}
495
496fn read_analysis_cache(path: PathBuf, fingerprint: &str, root: &Path) -> Option<FileAnalysis> {
497    let mut cached = serde_json::from_slice::<CachedRustAnalysis>(&fs::read(path).ok()?).ok()?;
498    if cached.schema != RUST_SEMANTIC_CACHE_SCHEMA || cached.fingerprint != fingerprint {
499        return None;
500    }
501    remap_analysis_root(&mut cached.analysis, &cached.root, root);
502    Some(cached.analysis)
503}
504
505fn write_analysis_cache(path: PathBuf, cached: &CachedRustAnalysis) -> std::io::Result<()> {
506    if let Some(parent) = path.parent() {
507        fs::create_dir_all(parent)?;
508    }
509    let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
510    let temporary = path.with_extension(format!("json.tmp-{}-{sequence}", std::process::id()));
511    fs::write(&temporary, serde_json::to_vec(cached)?)?;
512    if let Err(error) = fs::rename(&temporary, &path) {
513        if path.is_file() {
514            fs::remove_file(&path)?;
515            fs::rename(&temporary, &path)?;
516        } else {
517            let _ = fs::remove_file(temporary);
518            return Err(error);
519        }
520    }
521    Ok(())
522}
523
524struct RustProjectCapture {
525    directory: PathBuf,
526}
527
528impl RustProjectCapture {
529    fn create() -> std::io::Result<Self> {
530        let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
531        let directory = env::temp_dir().join(format!(
532            "diffkit-rust-capture-{}-{sequence}",
533            std::process::id()
534        ));
535        fs::create_dir(&directory)?;
536        fs::create_dir(directory.join("results"))?;
537        Ok(Self { directory })
538    }
539
540    fn results(&self) -> PathBuf {
541        self.directory.join("results")
542    }
543}
544
545impl Drop for RustProjectCapture {
546    fn drop(&mut self) {
547        let _ = fs::remove_dir_all(&self.directory);
548    }
549}
550
551/// Owns one Cargo target directory for one semantic-analysis endpoint.
552///
553/// A before/after comparison must not share a persistent target directory:
554/// Cargo identifies workspace packages independently of the temporary Git
555/// snapshot path, so a binary from one endpoint can otherwise be checked
556/// against stale library metadata from the other endpoint.
557pub(crate) struct RustProjectSession {
558    target: PathBuf,
559    cleanup: Option<PathBuf>,
560    endpoint: Option<String>,
561    verbose: bool,
562}
563
564impl RustProjectSession {
565    pub(crate) fn create() -> std::io::Result<Self> {
566        let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
567        let directory = env::temp_dir().join(format!(
568            "diffkit-rust-session-{}-{sequence}",
569            std::process::id()
570        ));
571        fs::create_dir(&directory)?;
572        Ok(Self {
573            target: directory.join("target"),
574            cleanup: Some(directory),
575            endpoint: None,
576            verbose: false,
577        })
578    }
579
580    pub(crate) fn create_cached(
581        project_root: &Path,
582        endpoint: &str,
583        verbose: bool,
584    ) -> std::io::Result<Self> {
585        let mut components = Path::new(endpoint).components();
586        if !matches!(components.next(), Some(std::path::Component::Normal(_)))
587            || components.next().is_some()
588        {
589            return Err(std::io::Error::new(
590                std::io::ErrorKind::InvalidInput,
591                "Rust semantic cache endpoint must be one path component",
592            ));
593        }
594        let target = project_root
595            .join("target")
596            .join("diffkit-semantic")
597            .join(endpoint);
598        fs::create_dir_all(&target)?;
599        Ok(Self {
600            target,
601            cleanup: None,
602            endpoint: Some(endpoint.to_owned()),
603            verbose,
604        })
605    }
606
607    fn target(&self) -> &Path {
608        &self.target
609    }
610
611    fn cache_path(&self) -> Option<PathBuf> {
612        self.cleanup
613            .is_none()
614            .then(|| self.target.join(".diffkit").join("analysis-v2.json"))
615    }
616
617    fn report_cache(
618        &self,
619        hit: bool,
620        fingerprint_key: &str,
621        fingerprint: std::time::Duration,
622        total: std::time::Duration,
623    ) {
624        if self.verbose {
625            let status = if hit { "hit" } else { "miss" };
626            eprintln!(
627                "Rust semantic cache {status}: {} [{}] ({} ms; fingerprint {} ms)",
628                self.endpoint.as_deref().unwrap_or("temporary"),
629                &fingerprint_key[..fingerprint_key.len().min(12)],
630                total.as_millis(),
631                fingerprint.as_millis(),
632            );
633        }
634    }
635}
636
637impl Drop for RustProjectSession {
638    fn drop(&mut self) {
639        if let Some(directory) = &self.cleanup {
640            let _ = fs::remove_dir_all(directory);
641        }
642    }
643}
644
645struct SeededRustProject {
646    directory: PathBuf,
647    root: PathBuf,
648}
649
650impl SeededRustProject {
651    fn create(source: &Path) -> std::io::Result<Self> {
652        let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
653        let directory = env::temp_dir().join(format!(
654            "diffkit-rust-seeded-{}-{sequence}",
655            std::process::id()
656        ));
657        let root = directory.join("project");
658        fs::create_dir_all(&root)?;
659        copy_rust_project(source, source, &root)?;
660        Ok(Self { directory, root })
661    }
662
663    fn root(&self) -> &Path {
664        &self.root
665    }
666}
667
668impl Drop for SeededRustProject {
669    fn drop(&mut self) {
670        let _ = fs::remove_dir_all(&self.directory);
671    }
672}
673
674fn copy_rust_project(
675    source_root: &Path,
676    directory: &Path,
677    destination: &Path,
678) -> std::io::Result<()> {
679    let mut entries = fs::read_dir(directory)?.collect::<Result<Vec<_>, _>>()?;
680    entries.sort_by_key(std::fs::DirEntry::path);
681    for entry in entries {
682        let path = entry.path();
683        let relative = path.strip_prefix(source_root).unwrap_or(&path);
684        if relative.components().any(|component| {
685            matches!(
686                component.as_os_str().to_str(),
687                Some(".git" | "target" | "node_modules" | "_build" | ".zig-cache" | "zig-out")
688            )
689        }) {
690            continue;
691        }
692        let target = destination.join(relative);
693        let file_type = entry.file_type()?;
694        if file_type.is_dir() {
695            fs::create_dir_all(&target)?;
696            copy_rust_project(source_root, &path, destination)?;
697        } else if file_type.is_file() {
698            if let Some(parent) = target.parent() {
699                fs::create_dir_all(parent)?;
700            }
701            fs::copy(path, target)?;
702        } else if file_type.is_symlink() {
703            let resolved = fs::canonicalize(path)?;
704            if resolved.starts_with(source_root) && resolved.is_file() {
705                if let Some(parent) = target.parent() {
706                    fs::create_dir_all(parent)?;
707                }
708                fs::copy(resolved, target)?;
709            }
710        }
711    }
712    Ok(())
713}
714
715fn seed_project_entries(root: &Path, entries: &[String]) -> FrontendResult<()> {
716    let mut files = Vec::new();
717    collect_rust_sources(root, &mut files)?;
718    files.sort();
719    for file in files {
720        let source = fs::read_to_string(&file)?;
721        if let Some(seeded) = append_entry_seeds(&source, entries)? {
722            fs::write(file, seeded)?;
723        }
724    }
725    Ok(())
726}
727
728fn remap_analysis_root(analysis: &mut FileAnalysis, from: &Path, to: &Path) {
729    let remap = |path: &mut PathBuf| {
730        if let Ok(relative) = path.strip_prefix(from) {
731            *path = to.join(relative);
732        }
733    };
734    for function in &mut analysis.functions {
735        remap(&mut function.span.file);
736        for call in &mut function.calls {
737            remap(&mut call.span.file);
738        }
739    }
740    for fact in &mut analysis.facts {
741        remap(&mut fact.span.file);
742    }
743    analysis.source_files = std::mem::take(&mut analysis.source_files)
744        .into_iter()
745        .map(|mut file| {
746            remap(&mut file);
747            file
748        })
749        .collect();
750}
751
752fn remove_seed_functions(analysis: &mut FileAnalysis) {
753    let seeds = analysis
754        .functions
755        .iter()
756        .filter(|function| function.label.default.contains("__diffkit_seed_"))
757        .map(|function| function.id.clone())
758        .collect::<HashSet<_>>();
759    analysis
760        .functions
761        .retain(|function| !seeds.contains(&function.id));
762    analysis.facts.retain(|fact| !seeds.contains(&fact.subject));
763}
764
765fn analyze_project_sources(root: &Path) -> FrontendResult<FileAnalysis> {
766    let mut files = Vec::new();
767    collect_rust_sources(root, &mut files)?;
768    files.sort();
769    let mut combined = FileAnalysis::default();
770    for file in files {
771        let source = fs::read_to_string(&file)?;
772        let module = rust_module_path(root, &file);
773        let mut analysis = RustBackend.analyze_file(
774            &FileContext {
775                path: &file,
776                module: &module,
777            },
778            &source,
779        )?;
780        combined.functions.append(&mut analysis.functions);
781        combined.facts.append(&mut analysis.facts);
782        combined.source_files.append(&mut analysis.source_files);
783    }
784    Ok(combined)
785}
786
787fn collect_rust_sources(directory: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
788    let mut entries = fs::read_dir(directory)?.collect::<Result<Vec<_>, _>>()?;
789    entries.sort_by_key(std::fs::DirEntry::path);
790    for entry in entries {
791        let path = entry.path();
792        let file_type = entry.file_type()?;
793        if file_type.is_dir() {
794            if !matches!(
795                path.file_name().and_then(|name| name.to_str()),
796                Some(".git" | "target" | "node_modules" | ".zig-cache" | "zig-out" | "_build")
797            ) {
798                collect_rust_sources(&path, files)?;
799            }
800        } else if file_type.is_file()
801            && path.extension().and_then(|extension| extension.to_str()) == Some("rs")
802        {
803            files.push(path);
804        }
805    }
806    Ok(())
807}
808
809fn rust_module_path(root: &Path, file: &Path) -> Vec<String> {
810    let source_root = root.join("src");
811    let relative = file
812        .strip_prefix(&source_root)
813        .or_else(|_| file.strip_prefix(root))
814        .unwrap_or(file);
815    let mut parts = relative
816        .parent()
817        .into_iter()
818        .flat_map(Path::components)
819        .filter_map(|component| component.as_os_str().to_str().map(ToOwned::to_owned))
820        .collect::<Vec<_>>();
821    let stem = file
822        .file_stem()
823        .and_then(|stem| stem.to_str())
824        .unwrap_or_default();
825    if !matches!(stem, "lib" | "main" | "mod") {
826        parts.push(stem.to_owned());
827    }
828    parts
829}
830
831fn deduplicate_analysis(analysis: FileAnalysis) -> FileAnalysis {
832    let mut functions = BTreeMap::<SymbolId, FunctionInfo>::new();
833    for function in analysis.functions {
834        functions
835            .entry(function.id.clone())
836            .and_modify(|existing| {
837                for call in &function.calls {
838                    if !existing.calls.contains(call) {
839                        existing.calls.push(call.clone());
840                    }
841                }
842                existing.calls.sort_by_key(|call| {
843                    (
844                        call.span.file.clone(),
845                        call.span.start_line,
846                        call.span.start_column,
847                    )
848                });
849            })
850            .or_insert(function);
851    }
852    FileAnalysis {
853        functions: functions.into_values().collect(),
854        facts: analysis.facts,
855        source_files: analysis.source_files,
856        roots: analysis.roots,
857    }
858}
859
860fn analysis_has_entry(analysis: &FileAnalysis, entry: &str) -> bool {
861    let expected = entry.replace('.', "::").replace("::<", "<");
862    analysis.functions.iter().any(|function| {
863        let label = &function.label.default;
864        let callable =
865            outer_call_arguments_start(label).map_or(label.as_str(), |index| &label[..index]);
866        callable == expected || callable.ends_with(&format!("::{expected}"))
867    })
868}
869
870fn append_entry_seeds(source: &str, entries: &[String]) -> FrontendResult<Option<String>> {
871    let file = syn::parse_file(source)?;
872    let mut functions = Vec::new();
873    collect_seedable_functions(&file.items, &[], &mut functions);
874    let mut seeds = Vec::new();
875
876    for entry in entries {
877        let Some(generic_start) = entry.find('<') else {
878            continue;
879        };
880        if !entry.ends_with('>') || entry.starts_with('<') {
881            continue;
882        }
883        let base = entry[..generic_start]
884            .trim_end_matches("::")
885            .replace('.', "::");
886        let generic_arguments = &entry[generic_start + 1..entry.len() - 1];
887        let lookup = base.strip_prefix("crate::").unwrap_or(&base);
888        let mut matches = functions
889            .iter()
890            .filter(|function| {
891                lookup == function.qualified_name
892                    || (!lookup.contains("::") && lookup == function.name)
893            })
894            .collect::<Vec<_>>();
895        if matches.len() != 1 {
896            continue;
897        }
898        let function = matches.pop().expect("one seedable function matched");
899        let arguments = (0..function.argument_count)
900            .map(|_| "::core::mem::MaybeUninit::uninit().assume_init()")
901            .collect::<Vec<_>>()
902            .join(", ");
903        seeds.push(format!(
904            "\n#[doc(hidden)]\n#[allow(dead_code, invalid_value, unused_unsafe)]\nfn __diffkit_seed_{}() {{\n    unsafe {{ let _ = {}::<{}>({}); }}\n}}\n",
905            seeds.len(), function.qualified_name, generic_arguments, arguments
906        ));
907    }
908
909    if seeds.is_empty() {
910        Ok(None)
911    } else {
912        let mut seeded = source.to_owned();
913        seeded.extend(seeds);
914        Ok(Some(seeded))
915    }
916}
917
918struct SeedableFunction {
919    name: String,
920    qualified_name: String,
921    argument_count: usize,
922}
923
924fn collect_seedable_functions(
925    items: &[Item],
926    module: &[String],
927    functions: &mut Vec<SeedableFunction>,
928) {
929    for item in items {
930        match item {
931            Item::Fn(function) => {
932                let name = function.sig.ident.to_string();
933                let qualified_name = module
934                    .iter()
935                    .map(String::as_str)
936                    .chain(std::iter::once(name.as_str()))
937                    .collect::<Vec<_>>()
938                    .join("::");
939                functions.push(SeedableFunction {
940                    name,
941                    qualified_name,
942                    argument_count: function.sig.inputs.len(),
943                });
944            }
945            Item::Mod(item_mod) => {
946                if let Some((_, nested)) = &item_mod.content {
947                    let mut nested_module = module.to_vec();
948                    nested_module.push(item_mod.ident.to_string());
949                    collect_seedable_functions(nested, &nested_module, functions);
950                }
951            }
952            _ => {}
953        }
954    }
955}
956
957impl TemporaryRustSource {
958    fn create(source: &str) -> std::io::Result<Self> {
959        let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
960        let directory = std::env::temp_dir().join(format!(
961            "diffkit-rust-source-{}-{sequence}",
962            std::process::id()
963        ));
964        fs::create_dir(&directory)?;
965        let temporary = TemporaryRustSource { directory };
966        let path = temporary.directory.join("input.rs");
967        fs::write(&path, source)?;
968        Ok(temporary)
969    }
970
971    fn path(&self) -> PathBuf {
972        self.directory.join("input.rs")
973    }
974}
975
976struct TemporaryRustSource {
977    directory: PathBuf,
978}
979
980impl Drop for TemporaryRustSource {
981    fn drop(&mut self) {
982        let _ = fs::remove_file(self.directory.join("input.rs"));
983        let _ = fs::remove_dir(&self.directory);
984    }
985}
986
987#[derive(Clone, Debug, Deserialize, Serialize)]
988struct SemanticProgram {
989    crate_name: String,
990    functions: Vec<SemanticFunction>,
991}
992
993#[derive(Debug, Deserialize, Serialize)]
994struct CachedRustAnalysis {
995    schema: u32,
996    fingerprint: String,
997    root: PathBuf,
998    analysis: FileAnalysis,
999}
1000
1001#[derive(Clone, Debug, Deserialize, Serialize)]
1002struct SemanticFunction {
1003    base_key: String,
1004    key: String,
1005    display: String,
1006    definition_name: String,
1007    body_span: SourceSpan,
1008    calls: Vec<SemanticCall>,
1009    constructor_spans: Vec<SourceSpan>,
1010    constructor_names: Vec<String>,
1011    parameter_types: Vec<String>,
1012}
1013
1014#[derive(Clone, Debug, Deserialize, Serialize)]
1015struct SemanticCall {
1016    target: SemanticCallTarget,
1017    definition_name: String,
1018    span: SourceSpan,
1019    argument_types: Vec<String>,
1020    argument_flows: Vec<SemanticDynFlow>,
1021}
1022
1023#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1024struct SemanticDynFlow {
1025    concrete_types: Vec<String>,
1026    parameters: Vec<usize>,
1027    unresolved_reasons: BTreeSet<UnresolvedReason>,
1028}
1029
1030#[derive(Clone, Debug, Deserialize, Serialize)]
1031enum SemanticCallTarget {
1032    Direct {
1033        key: String,
1034        display: String,
1035    },
1036    Dynamic {
1037        key: String,
1038        display: String,
1039        candidates: Vec<SemanticDispatchCandidate>,
1040        open: bool,
1041        unresolved_reasons: BTreeSet<UnresolvedReason>,
1042        receiver_flow: SemanticDynFlow,
1043    },
1044    Indirect {
1045        signature: String,
1046    },
1047    Unresolved,
1048}
1049
1050#[derive(Clone, Debug, Deserialize, Serialize)]
1051struct SemanticDispatchCandidate {
1052    key: String,
1053    display: String,
1054}
1055
1056#[derive(Clone, Debug, Default, Eq, PartialEq)]
1057struct DynFlow {
1058    concrete: Vec<TraitRef>,
1059    parameters: Vec<usize>,
1060    unresolved_reasons: BTreeSet<UnresolvedReason>,
1061}
1062
1063impl DynFlow {
1064    fn parameter(index: usize) -> Self {
1065        Self {
1066            concrete: Vec::new(),
1067            parameters: vec![index],
1068            unresolved_reasons: BTreeSet::new(),
1069        }
1070    }
1071
1072    fn concrete(trait_ref: TraitRef) -> Self {
1073        Self {
1074            concrete: vec![trait_ref],
1075            parameters: Vec::new(),
1076            unresolved_reasons: BTreeSet::new(),
1077        }
1078    }
1079
1080    fn unresolved(reason: UnresolvedReason) -> Self {
1081        Self {
1082            concrete: Vec::new(),
1083            parameters: Vec::new(),
1084            unresolved_reasons: [reason].into_iter().collect(),
1085        }
1086    }
1087
1088    fn is_empty(&self) -> bool {
1089        self.concrete.is_empty() && self.parameters.is_empty() && self.unresolved_reasons.is_empty()
1090    }
1091
1092    fn merge(&mut self, other: &Self) -> bool {
1093        let mut changed = false;
1094        for candidate in &other.concrete {
1095            if !self.concrete.contains(candidate) {
1096                self.concrete.push(candidate.clone());
1097                changed = true;
1098            }
1099        }
1100        for parameter in &other.parameters {
1101            if !self.parameters.contains(parameter) {
1102                self.parameters.push(*parameter);
1103                changed = true;
1104            }
1105        }
1106        for reason in &other.unresolved_reasons {
1107            changed |= self.unresolved_reasons.insert(reason.clone());
1108        }
1109        self.parameters.sort_unstable();
1110        changed
1111    }
1112}
1113
1114#[derive(Clone, Debug, Default)]
1115struct ResolvedDynFlow {
1116    concrete: Vec<TraitRef>,
1117    unresolved_reasons: BTreeSet<UnresolvedReason>,
1118}
1119
1120impl ResolvedDynFlow {
1121    fn merge(&mut self, other: &Self) {
1122        for candidate in &other.concrete {
1123            if !self.concrete.contains(candidate) {
1124                self.concrete.push(candidate.clone());
1125            }
1126        }
1127        self.unresolved_reasons
1128            .extend(other.unresolved_reasons.iter().cloned());
1129    }
1130}
1131
1132#[derive(Clone)]
1133struct RawSemanticFunction {
1134    key: String,
1135    display: String,
1136    definition_name: String,
1137    body_span: SourceSpan,
1138    calls: Vec<RawSemanticCall>,
1139    constructor_spans: Vec<SourceSpan>,
1140    constructor_names: Vec<String>,
1141    parameter_types: Vec<String>,
1142}
1143
1144#[derive(Clone)]
1145struct RawSemanticCall {
1146    target: RawSemanticCallTarget,
1147    definition_name: String,
1148    span: SourceSpan,
1149    argument_types: Vec<String>,
1150    argument_flows: Vec<DynFlow>,
1151}
1152
1153#[derive(Clone)]
1154enum RawSemanticCallTarget {
1155    Direct {
1156        key: String,
1157        display: String,
1158    },
1159    Dynamic {
1160        dispatch: Instance,
1161        key: String,
1162        display: String,
1163        receiver: DynFlow,
1164    },
1165    Indirect {
1166        signature: String,
1167    },
1168    Unresolved,
1169}
1170
1171#[derive(Clone)]
1172struct InstanceBody {
1173    instance: Instance,
1174    body: Body,
1175}
1176
1177#[derive(Clone, Default)]
1178struct MirCallFlow {
1179    arguments: Vec<DynFlow>,
1180}
1181
1182#[derive(Default)]
1183struct BodyDynAnalysis {
1184    calls: Vec<Option<MirCallFlow>>,
1185    return_flow: DynFlow,
1186}
1187
1188fn collect_rustc_program(path: &Path) -> FrontendResult<SemanticProgram> {
1189    let _driver_guard = RUSTC_DRIVER_LOCK
1190        .lock()
1191        .map_err(|_| std::io::Error::other("rustc semantic driver lock was poisoned"))?;
1192    let arguments = vec![
1193        "rustc".to_owned(),
1194        path.display().to_string(),
1195        "--crate-name=diffkit_input".to_owned(),
1196        "--crate-type=lib".to_owned(),
1197        "--edition=2024".to_owned(),
1198        "--cap-lints=allow".to_owned(),
1199    ];
1200
1201    match rustc_public::run!(&arguments, || {
1202        std::ops::ControlFlow::<SemanticProgram, ()>::Break(collect_instances())
1203    }) {
1204        Err(CompilerError::Interrupted(program)) => Ok(program),
1205        Err(CompilerError::Failed) => Err(std::io::Error::other(format!(
1206            "rustc failed to compile {} for semantic analysis",
1207            path.display()
1208        ))
1209        .into()),
1210        Err(CompilerError::Skipped) => Err(std::io::Error::other(
1211            "rustc skipped semantic analysis before its callback ran",
1212        )
1213        .into()),
1214        Ok(()) => Err(std::io::Error::other(
1215            "rustc semantic callback unexpectedly completed without analysis",
1216        )
1217        .into()),
1218    }
1219}
1220
1221fn collect_instances() -> SemanticProgram {
1222    let crate_name = rustc_public::local_crate().name.to_string();
1223    let trait_method_implementations = trait_method_implementations();
1224    let (instance_bodies, observed_vtables) =
1225        collect_instance_bodies(&trait_method_implementations);
1226    let local_instances = instance_bodies
1227        .iter()
1228        .map(|item| item.instance)
1229        .collect::<HashSet<_>>();
1230    let mut return_summaries = HashMap::<Instance, DynFlow>::new();
1231
1232    // Return-value summaries remain symbolic in terms of the callee's formal
1233    // parameters. This lets a trait object cross ordinary helper functions
1234    // without losing its concrete provenance. The lattice is finite, so the
1235    // monotone iteration also handles recursive helpers.
1236    let max_iterations = instance_bodies
1237        .iter()
1238        .map(|item| item.body.locals().len())
1239        .sum::<usize>()
1240        .saturating_add(instance_bodies.len())
1241        .max(1);
1242    let mut summaries_converged = false;
1243    for _ in 0..max_iterations {
1244        let mut changed = false;
1245        for item in &instance_bodies {
1246            let analysis = analyze_body_dyn_flows(&item.body, &return_summaries, &local_instances);
1247            changed |= return_summaries
1248                .entry(item.instance)
1249                .or_default()
1250                .merge(&analysis.return_flow);
1251        }
1252        if !changed {
1253            summaries_converged = true;
1254            break;
1255        }
1256    }
1257    if !summaries_converged {
1258        for item in &instance_bodies {
1259            if type_contains_dyn(item.body.ret_local().ty) {
1260                return_summaries
1261                    .entry(item.instance)
1262                    .or_default()
1263                    .unresolved_reasons
1264                    .insert(UnresolvedReason::AnalysisLimit);
1265            }
1266        }
1267    }
1268
1269    let mut raw_functions = Vec::new();
1270    for item in &instance_bodies {
1271        let instance = item.instance;
1272        let body = &item.body;
1273        let instance_name = stable_instance_name(instance);
1274        let analysis = analyze_body_dyn_flows(body, &return_summaries, &local_instances);
1275        let mut calls = Vec::new();
1276        for (block_index, block) in body.blocks.iter().enumerate() {
1277            let TerminatorKind::Call { func, args, .. } = &block.terminator.kind else {
1278                continue;
1279            };
1280            let resolved_target = resolve_called_instance(body, func);
1281            let definition_name = resolved_target
1282                .map(|target| target.def.name())
1283                .unwrap_or_default();
1284            let call_flow = analysis
1285                .calls
1286                .get(block_index)
1287                .and_then(Option::as_ref)
1288                .cloned()
1289                .unwrap_or_default();
1290            let target = match resolved_target {
1291                Some(target) => {
1292                    let name = stable_instance_name(target);
1293                    match target.kind {
1294                        InstanceKind::Virtual { .. } => RawSemanticCallTarget::Dynamic {
1295                            dispatch: target,
1296                            key: normalize_instance_key(&name),
1297                            display: normalize_instance_display(&name, &crate_name),
1298                            receiver: call_flow.arguments.first().cloned().unwrap_or_else(|| {
1299                                DynFlow::unresolved(UnresolvedReason::AnalysisLimit)
1300                            }),
1301                        },
1302                        _ => RawSemanticCallTarget::Direct {
1303                            key: normalize_instance_key(&name),
1304                            display: normalize_instance_display(&name, &crate_name),
1305                        },
1306                    }
1307                }
1308                None => func
1309                    .ty(body.locals())
1310                    .ok()
1311                    .filter(|ty| matches!(ty.kind(), TyKind::RigidTy(RigidTy::FnPtr(_))))
1312                    .map(|ty| RawSemanticCallTarget::Indirect {
1313                        signature: normalize_type_display(&ty.to_string(), &crate_name),
1314                    })
1315                    .unwrap_or(RawSemanticCallTarget::Unresolved),
1316            };
1317            calls.push(RawSemanticCall {
1318                target,
1319                definition_name,
1320                span: rustc_source_span(block.terminator.span),
1321                argument_types: args
1322                    .iter()
1323                    .filter_map(|argument| argument.ty(body.locals()).ok())
1324                    .map(|ty| normalize_type_display(&ty.to_string(), &crate_name))
1325                    .collect(),
1326                argument_flows: call_flow.arguments,
1327            });
1328        }
1329        calls.sort_by_key(|call| {
1330            (
1331                call.span.start_line,
1332                call.span.start_column,
1333                call.span.end_line,
1334                call.span.end_column,
1335            )
1336        });
1337        let mut constructor_spans = body
1338            .blocks
1339            .iter()
1340            .flat_map(|block| &block.statements)
1341            .filter_map(|statement| {
1342                let StatementKind::Assign(_, value) = &statement.kind else {
1343                    return None;
1344                };
1345                let ty = value.ty(body.locals()).ok()?;
1346                matches!(ty.kind(), TyKind::RigidTy(RigidTy::Adt(..)))
1347                    .then(|| rustc_source_span(statement.span))
1348            })
1349            .collect::<Vec<_>>();
1350        constructor_spans.sort_by_key(|span| {
1351            (
1352                span.start_line,
1353                span.start_column,
1354                span.end_line,
1355                span.end_column,
1356            )
1357        });
1358        constructor_spans.dedup();
1359        let mut constructor_names = HashSet::new();
1360        for local in body.locals() {
1361            collect_adt_constructor_names(local.ty, &mut constructor_names);
1362        }
1363        let mut constructor_names = constructor_names.into_iter().collect::<Vec<_>>();
1364        constructor_names.sort();
1365        raw_functions.push(RawSemanticFunction {
1366            key: normalize_instance_key(&instance_name),
1367            display: normalize_instance_display(&instance_name, &crate_name),
1368            definition_name: instance.def.name().to_string(),
1369            body_span: rustc_source_span(body.span),
1370            calls,
1371            constructor_spans,
1372            constructor_names,
1373            parameter_types: body
1374                .arg_locals()
1375                .iter()
1376                .map(|argument| normalize_type_display(&argument.ty.to_string(), &crate_name))
1377                .collect(),
1378        });
1379    }
1380
1381    let mut functions = specialize_semantic_functions(
1382        &raw_functions,
1383        &observed_vtables,
1384        &trait_method_implementations,
1385        &crate_name,
1386    );
1387    functions.sort_by(|left, right| left.key.cmp(&right.key));
1388    SemanticProgram {
1389        crate_name,
1390        functions,
1391    }
1392}
1393
1394fn collect_instance_bodies(
1395    trait_method_implementations: &HashMap<rustc_public::DefId, rustc_public::DefId>,
1396) -> (Vec<InstanceBody>, Vec<TraitRef>) {
1397    let mut queue = rustc_public::all_local_items()
1398        .into_iter()
1399        .filter(|item| matches!(item.kind(), rustc_public::ItemKind::Fn))
1400        .filter_map(|item| Instance::try_from(item).ok())
1401        .collect::<VecDeque<_>>();
1402    let mut visited = HashSet::new();
1403    let mut bodies = Vec::new();
1404    let mut observed_vtables = Vec::<TraitRef>::new();
1405
1406    loop {
1407        while let Some(instance) = queue.pop_front() {
1408            if !visited.insert(instance) {
1409                continue;
1410            }
1411            let Some(body) = instance.body() else {
1412                continue;
1413            };
1414
1415            let mut local_vtables = Vec::new();
1416            collect_observed_vtables(&body, &mut local_vtables);
1417            for vtable in local_vtables {
1418                if !observed_vtables.contains(&vtable) {
1419                    observed_vtables.push(vtable);
1420                }
1421            }
1422
1423            for block in &body.blocks {
1424                let TerminatorKind::Call {
1425                    func,
1426                    args,
1427                    destination,
1428                    ..
1429                } = &block.terminator.kind
1430                else {
1431                    continue;
1432                };
1433                let Some(target) = resolve_called_instance(&body, func) else {
1434                    continue;
1435                };
1436                if !matches!(target.kind, InstanceKind::Virtual { .. })
1437                    && target.has_body()
1438                    && (target.def.krate().is_local
1439                        || instance_body_is_in_analysis_root(target)
1440                        || call_boundary_contains_dyn(&body, args, destination))
1441                {
1442                    queue.push_back(target);
1443                }
1444            }
1445            bodies.push(InstanceBody { instance, body });
1446        }
1447
1448        for item in &bodies {
1449            for block in &item.body.blocks {
1450                let TerminatorKind::Call { func, .. } = &block.terminator.kind else {
1451                    continue;
1452                };
1453                let Some(dispatch) = resolve_called_instance(&item.body, func) else {
1454                    continue;
1455                };
1456                if !matches!(dispatch.kind, InstanceKind::Virtual { .. }) {
1457                    continue;
1458                }
1459                for trait_ref in &observed_vtables {
1460                    let Some(candidate) = resolve_dispatch_candidate(
1461                        dispatch,
1462                        trait_ref,
1463                        trait_method_implementations,
1464                    ) else {
1465                        continue;
1466                    };
1467                    if candidate.has_body() && !visited.contains(&candidate) {
1468                        queue.push_back(candidate);
1469                    }
1470                }
1471            }
1472        }
1473        if queue.is_empty() {
1474            break;
1475        }
1476    }
1477    (bodies, observed_vtables)
1478}
1479
1480fn instance_body_is_in_analysis_root(instance: Instance) -> bool {
1481    let Some(root) = env::var_os(RUSTC_ANALYSIS_ROOT).map(PathBuf::from) else {
1482        return false;
1483    };
1484    let Some(body) = instance.body() else {
1485        return false;
1486    };
1487    let file = rustc_source_span(body.span).file;
1488    file.starts_with(&root)
1489        || file
1490            .canonicalize()
1491            .ok()
1492            .zip(root.canonicalize().ok())
1493            .is_some_and(|(file, root)| file.starts_with(root))
1494}
1495
1496fn call_boundary_contains_dyn(body: &Body, arguments: &[Operand], destination: &Place) -> bool {
1497    arguments
1498        .iter()
1499        .filter_map(|argument| argument.ty(body.locals()).ok())
1500        .any(type_contains_dyn)
1501        || destination.ty(body.locals()).is_ok_and(type_contains_dyn)
1502}
1503
1504fn collect_observed_vtables(body: &rustc_public::mir::Body, observed: &mut Vec<TraitRef>) {
1505    for block in &body.blocks {
1506        for statement in &block.statements {
1507            let StatementKind::Assign(
1508                _,
1509                Rvalue::Cast(
1510                    CastKind::PointerCoercion(PointerCoercion::Unsize),
1511                    operand,
1512                    target_ty,
1513                ),
1514            ) = &statement.kind
1515            else {
1516                continue;
1517            };
1518            let Ok(source_ty) = operand.ty(body.locals()) else {
1519                continue;
1520            };
1521            let Some((concrete_ty, principal)) = dyn_coercion(source_ty, *target_ty) else {
1522                continue;
1523            };
1524            // `TraitRef` cannot represent the binder around a higher-ranked
1525            // principal such as `dyn for<'a> Fn(&'a T)`. Flattening it would
1526            // pass escaping bound variables to rustc's vtable query and panic.
1527            if !principal.bound_vars.is_empty() {
1528                continue;
1529            }
1530            let principal = principal.value;
1531            let trait_ref = TraitRef::new(principal.def_id, concrete_ty, &principal.generic_args);
1532            if !observed.contains(&trait_ref) {
1533                observed.push(trait_ref);
1534            }
1535        }
1536    }
1537}
1538
1539fn dyn_coercion(source: Ty, target: Ty) -> Option<(Ty, Binder<ExistentialTraitRef>)> {
1540    let target_kind = target.kind();
1541    if let Some(principal) = target_kind.trait_principal() {
1542        // A trait upcast (`dyn Child` -> `dyn Parent`) does not reveal a
1543        // concrete implementation. Only thin-to-wide coercions contribute an
1544        // concrete receiver-flow candidate here.
1545        return (!source.kind().is_trait()).then_some((source, principal));
1546    }
1547
1548    match (source.kind(), target_kind) {
1549        (
1550            TyKind::RigidTy(RigidTy::Ref(_, source, _)),
1551            TyKind::RigidTy(RigidTy::Ref(_, target, _)),
1552        )
1553        | (
1554            TyKind::RigidTy(RigidTy::RawPtr(source, _)),
1555            TyKind::RigidTy(RigidTy::RawPtr(target, _)),
1556        ) => dyn_coercion(source, target),
1557        (
1558            TyKind::RigidTy(RigidTy::Adt(source_def, source_args)),
1559            TyKind::RigidTy(RigidTy::Adt(target_def, target_args)),
1560        ) if source_def == target_def => source_args
1561            .0
1562            .iter()
1563            .zip(&target_args.0)
1564            .filter_map(|(source, target)| Some((*source.ty()?, *target.ty()?)))
1565            .find_map(|(source, target)| dyn_coercion(source, target)),
1566        _ => None,
1567    }
1568}
1569
1570fn trait_method_implementations() -> HashMap<rustc_public::DefId, rustc_public::DefId> {
1571    rustc_public::all_trait_impls()
1572        .into_iter()
1573        .flat_map(|implementation| implementation.associated_items())
1574        .filter_map(|item| match item.container {
1575            AssocContainer::TraitImpl(trait_item) => {
1576                Some((item.def_id.def_id(), trait_item.def_id()))
1577            }
1578            AssocContainer::InherentImpl | AssocContainer::Trait => None,
1579        })
1580        .collect()
1581}
1582
1583fn resolve_called_instance(body: &Body, operand: &Operand) -> Option<Instance> {
1584    let function_type = operand.ty(body.locals()).ok()?;
1585    let function_kind = function_type.kind();
1586    let (definition, arguments) = function_kind.fn_def()?;
1587    Instance::resolve(definition, arguments).ok()
1588}
1589
1590fn resolve_dispatch_candidate(
1591    dispatch: Instance,
1592    trait_ref: &TraitRef,
1593    trait_method_implementations: &HashMap<rustc_public::DefId, rustc_public::DefId>,
1594) -> Option<Instance> {
1595    let InstanceKind::Virtual { idx } = dispatch.kind else {
1596        return None;
1597    };
1598    let VtblEntry::Method(candidate) = trait_ref.vtable_entry(idx)? else {
1599        return None;
1600    };
1601    let trait_method = dispatch.def.def_id();
1602    let candidate_method = candidate.def.def_id();
1603    (candidate_method == trait_method
1604        || trait_method_implementations.get(&candidate_method) == Some(&trait_method))
1605    .then_some(candidate)
1606}
1607
1608#[derive(Clone, Default)]
1609struct DynState {
1610    flows: HashMap<Place, DynFlow>,
1611    aliases: HashMap<Place, Vec<Place>>,
1612}
1613
1614fn analyze_body_dyn_flows(
1615    body: &Body,
1616    return_summaries: &HashMap<Instance, DynFlow>,
1617    local_instances: &HashSet<Instance>,
1618) -> BodyDynAnalysis {
1619    let mut result = BodyDynAnalysis {
1620        calls: vec![None; body.blocks.len()],
1621        return_flow: DynFlow::default(),
1622    };
1623    if body.blocks.is_empty() {
1624        return result;
1625    }
1626
1627    let mut entry = DynState::default();
1628    for (index, argument) in body.arg_locals().iter().enumerate() {
1629        if type_contains_dyn(argument.ty) {
1630            entry
1631                .flows
1632                .insert(Place::from(index + 1), DynFlow::parameter(index));
1633        }
1634    }
1635    let mut incoming = vec![None::<DynState>; body.blocks.len()];
1636    incoming[0] = Some(entry);
1637    let mut pending = VecDeque::from([0usize]);
1638
1639    let mut iterations = 0usize;
1640    let max_iterations = body
1641        .blocks
1642        .len()
1643        .saturating_mul(body.locals().len().saturating_add(1))
1644        .saturating_mul(16)
1645        .max(1);
1646    while let Some(block_index) = pending.pop_front() {
1647        iterations += 1;
1648        if iterations > max_iterations {
1649            degrade_dyn_analysis_to_unknown(body, &mut result);
1650            break;
1651        }
1652        let Some(mut state) = incoming[block_index].clone() else {
1653            continue;
1654        };
1655        let block = &body.blocks[block_index];
1656        for statement in &block.statements {
1657            transfer_dyn_statement(body, &mut state, statement);
1658        }
1659
1660        match &block.terminator.kind {
1661            TerminatorKind::Call {
1662                func,
1663                args,
1664                destination,
1665                target,
1666                ..
1667            } => {
1668                let argument_flows = args
1669                    .iter()
1670                    .map(|argument| dyn_flow_for_operand(body, &state, argument))
1671                    .collect::<Vec<_>>();
1672                merge_call_flow(
1673                    &mut result.calls[block_index],
1674                    &MirCallFlow {
1675                        arguments: argument_flows.clone(),
1676                    },
1677                );
1678
1679                let destination_flow = resolve_called_instance(body, func).map_or_else(
1680                    || unknown_if_dyn_place(body, destination, UnresolvedReason::ExternalCode),
1681                    |callee| {
1682                        if matches!(callee.kind, InstanceKind::Virtual { .. }) {
1683                            unknown_if_dyn_place(body, destination, UnresolvedReason::ExternalCode)
1684                        } else if let Some(summary) = return_summaries.get(&callee) {
1685                            let mut flow = resolve_symbolic_flow(summary, &argument_flows);
1686                            if !callee.def.krate().is_local {
1687                                flow.merge(&external_return_flow(
1688                                    body,
1689                                    args,
1690                                    destination,
1691                                    &argument_flows,
1692                                ));
1693                            }
1694                            flow
1695                        } else if local_instances.contains(&callee) {
1696                            DynFlow::default()
1697                        } else {
1698                            external_return_flow(body, args, destination, &argument_flows)
1699                        }
1700                    },
1701                );
1702
1703                apply_external_memory_effects(
1704                    body,
1705                    &mut state,
1706                    func,
1707                    args,
1708                    resolve_called_instance(body, func),
1709                    &argument_flows,
1710                );
1711
1712                for successor in block.terminator.successors() {
1713                    let mut outgoing = state.clone();
1714                    if Some(successor) == *target {
1715                        write_dyn_place(&mut outgoing, destination, destination_flow.clone());
1716                    }
1717                    if merge_dyn_state(&mut incoming[successor], &outgoing) {
1718                        pending.push_back(successor);
1719                    }
1720                }
1721            }
1722            TerminatorKind::Return => {
1723                let flow = read_dyn_place(&state, &Place::from(0));
1724                result.return_flow.merge(&flow);
1725            }
1726            _ => {
1727                for successor in block.terminator.successors() {
1728                    if merge_dyn_state(&mut incoming[successor], &state) {
1729                        pending.push_back(successor);
1730                    }
1731                }
1732            }
1733        }
1734    }
1735    result
1736}
1737
1738fn external_return_flow(
1739    body: &Body,
1740    arguments: &[Operand],
1741    destination: &Place,
1742    argument_flows: &[DynFlow],
1743) -> DynFlow {
1744    if !destination.ty(body.locals()).is_ok_and(type_contains_dyn) {
1745        return DynFlow::default();
1746    }
1747    let mut flow = DynFlow::unresolved(UnresolvedReason::ExternalCode);
1748    for (argument, argument_flow) in arguments.iter().zip(argument_flows) {
1749        if argument.ty(body.locals()).is_ok_and(type_contains_dyn) {
1750            flow.merge(argument_flow);
1751        }
1752    }
1753    flow
1754}
1755
1756/// Model memory that crosses a call boundary as an abstract region attached
1757/// to the referenced MIR place. This is type-directed: it does not name Vec,
1758/// Arc, HashMap, or individual methods. Opaque external behavior remains in
1759/// the unresolved-reason set, while concrete values that cross that same
1760/// boundary remain available as partial evidence instead of being lost.
1761fn apply_external_memory_effects(
1762    body: &Body,
1763    state: &mut DynState,
1764    _function: &Operand,
1765    arguments: &[Operand],
1766    resolved: Option<Instance>,
1767    argument_flows: &[DynFlow],
1768) {
1769    if resolved.is_some_and(|callee| callee.def.krate().is_local) {
1770        return;
1771    }
1772    let mut stored = DynFlow::default();
1773    for (argument, flow) in arguments.iter().zip(argument_flows) {
1774        let Ok(ty) = argument.ty(body.locals()) else {
1775            continue;
1776        };
1777        if !is_mutable_reference(ty) && type_contains_dyn(ty) {
1778            stored.merge(flow);
1779        }
1780    }
1781    if stored.is_empty() {
1782        return;
1783    }
1784    stored
1785        .unresolved_reasons
1786        .insert(UnresolvedReason::ExternalCode);
1787    for argument in arguments {
1788        let Ok(ty) = argument.ty(body.locals()) else {
1789            continue;
1790        };
1791        if !is_mutable_reference(ty) || !type_contains_dyn(ty) {
1792            continue;
1793        }
1794        let Some(place) = operand_place(argument) else {
1795            continue;
1796        };
1797        merge_dyn_place(state, place, &stored);
1798    }
1799}
1800
1801fn operand_place(operand: &Operand) -> Option<&Place> {
1802    match operand {
1803        Operand::Copy(place) | Operand::Move(place) => Some(place),
1804        Operand::Constant(_) | Operand::RuntimeChecks(_) => None,
1805    }
1806}
1807
1808fn is_mutable_reference(ty: Ty) -> bool {
1809    matches!(
1810        ty.kind(),
1811        TyKind::RigidTy(RigidTy::Ref(_, _, rustc_public::mir::Mutability::Mut))
1812            | TyKind::RigidTy(RigidTy::RawPtr(_, rustc_public::mir::Mutability::Mut))
1813    )
1814}
1815
1816fn degrade_dyn_analysis_to_unknown(body: &Body, analysis: &mut BodyDynAnalysis) {
1817    if type_contains_dyn(body.ret_local().ty) {
1818        analysis
1819            .return_flow
1820            .unresolved_reasons
1821            .insert(UnresolvedReason::AnalysisLimit);
1822    }
1823    for (block_index, block) in body.blocks.iter().enumerate() {
1824        let TerminatorKind::Call { args, .. } = &block.terminator.kind else {
1825            continue;
1826        };
1827        let call = analysis.calls[block_index].get_or_insert_with(|| MirCallFlow {
1828            arguments: vec![DynFlow::default(); args.len()],
1829        });
1830        call.arguments.resize_with(args.len(), DynFlow::default);
1831        for (argument, flow) in args.iter().zip(&mut call.arguments) {
1832            if argument.ty(body.locals()).is_ok_and(type_contains_dyn) {
1833                flow.unresolved_reasons
1834                    .insert(UnresolvedReason::AnalysisLimit);
1835            }
1836        }
1837    }
1838}
1839
1840fn transfer_dyn_statement(
1841    body: &Body,
1842    state: &mut DynState,
1843    statement: &rustc_public::mir::Statement,
1844) {
1845    let StatementKind::Assign(destination, value) = &statement.kind else {
1846        return;
1847    };
1848    match value {
1849        Rvalue::Ref(_, _, source) | Rvalue::AddressOf(_, source) | Rvalue::CopyForDeref(source) => {
1850            let targets = resolve_alias_places(state, source);
1851            state.aliases.insert(destination.clone(), targets);
1852        }
1853        Rvalue::Use(Operand::Copy(source) | Operand::Move(source), _)
1854        | Rvalue::Cast(_, Operand::Copy(source) | Operand::Move(source), _) => {
1855            if let Some(targets) = state.aliases.get(source).cloned() {
1856                state.aliases.insert(destination.clone(), targets);
1857            } else {
1858                state.aliases.remove(destination);
1859            }
1860        }
1861        _ => {
1862            state.aliases.remove(destination);
1863        }
1864    }
1865    let flow = dyn_flow_for_rvalue(body, state, value);
1866    write_dyn_place(state, destination, flow);
1867
1868    let Rvalue::Aggregate(kind, operands) = value else {
1869        return;
1870    };
1871    for (index, operand) in operands.iter().enumerate() {
1872        let Ok(operand_ty) = operand.ty(body.locals()) else {
1873            continue;
1874        };
1875        let projection = match kind {
1876            AggregateKind::Tuple | AggregateKind::Adt(..) => {
1877                Some(ProjectionElem::Field(index, operand_ty))
1878            }
1879            AggregateKind::Array(_) => Some(ProjectionElem::ConstantIndex {
1880                offset: index as u64,
1881                min_length: operands.len() as u64,
1882                from_end: false,
1883            }),
1884            AggregateKind::Closure(..)
1885            | AggregateKind::Coroutine(..)
1886            | AggregateKind::CoroutineClosure(..)
1887            | AggregateKind::RawPtr(..) => None,
1888        };
1889        let Some(projection) = projection else {
1890            continue;
1891        };
1892        let mut field = destination.clone();
1893        field.projection.push(projection);
1894        write_dyn_place(state, &field, dyn_flow_for_operand(body, state, operand));
1895    }
1896}
1897
1898fn dyn_flow_for_rvalue(body: &Body, state: &DynState, value: &Rvalue) -> DynFlow {
1899    let mut flow = match value {
1900        Rvalue::Use(operand, _) | Rvalue::Repeat(operand, _) => {
1901            dyn_flow_for_operand(body, state, operand)
1902        }
1903        Rvalue::Cast(CastKind::PointerCoercion(PointerCoercion::Unsize), operand, target_ty) => {
1904            operand
1905                .ty(body.locals())
1906                .ok()
1907                .and_then(|source_ty| dyn_coercion(source_ty, *target_ty))
1908                .and_then(|(concrete_ty, principal)| {
1909                    principal.bound_vars.is_empty().then(|| {
1910                        let principal = principal.value;
1911                        DynFlow::concrete(TraitRef::new(
1912                            principal.def_id,
1913                            concrete_ty,
1914                            &principal.generic_args,
1915                        ))
1916                    })
1917                })
1918                .unwrap_or_else(|| dyn_flow_for_operand(body, state, operand))
1919        }
1920        Rvalue::Cast(_, operand, _) => dyn_flow_for_operand(body, state, operand),
1921        Rvalue::Ref(_, _, place) | Rvalue::AddressOf(_, place) | Rvalue::CopyForDeref(place) => {
1922            read_dyn_place(state, place)
1923        }
1924        Rvalue::Aggregate(_, operands) => {
1925            let mut aggregate = DynFlow::default();
1926            for operand in operands {
1927                aggregate.merge(&dyn_flow_for_operand(body, state, operand));
1928            }
1929            aggregate
1930        }
1931        Rvalue::BinaryOp(_, left, right) | Rvalue::CheckedBinaryOp(_, left, right) => {
1932            let mut combined = dyn_flow_for_operand(body, state, left);
1933            combined.merge(&dyn_flow_for_operand(body, state, right));
1934            combined
1935        }
1936        Rvalue::UnaryOp(_, operand) => dyn_flow_for_operand(body, state, operand),
1937        Rvalue::ThreadLocalRef(_) | Rvalue::Discriminant(_) | Rvalue::Len(_) => DynFlow::default(),
1938    };
1939    if flow.is_empty() && value.ty(body.locals()).is_ok_and(type_contains_dyn) {
1940        flow.unresolved_reasons
1941            .insert(UnresolvedReason::AnalysisLimit);
1942    }
1943    flow
1944}
1945
1946fn dyn_flow_for_operand(body: &Body, state: &DynState, operand: &Operand) -> DynFlow {
1947    let mut flow = match operand {
1948        Operand::Copy(place) | Operand::Move(place) => read_dyn_place(state, place),
1949        Operand::Constant(_) | Operand::RuntimeChecks(_) => DynFlow::default(),
1950    };
1951    if flow.is_empty() && operand.ty(body.locals()).is_ok_and(type_contains_dyn) {
1952        flow.unresolved_reasons
1953            .insert(UnresolvedReason::ExternalMemory);
1954    }
1955    flow
1956}
1957
1958fn unknown_if_dyn_place(body: &Body, place: &Place, reason: UnresolvedReason) -> DynFlow {
1959    place
1960        .ty(body.locals())
1961        .ok()
1962        .filter(|ty| type_contains_dyn(*ty))
1963        .map_or_else(DynFlow::default, |_| DynFlow::unresolved(reason))
1964}
1965
1966fn type_contains_dyn(ty: Ty) -> bool {
1967    ty.to_string().contains("dyn ")
1968}
1969
1970fn collect_adt_constructor_names(ty: Ty, names: &mut HashSet<String>) {
1971    match ty.kind() {
1972        TyKind::RigidTy(RigidTy::Adt(definition, arguments)) => {
1973            names.extend(
1974                definition
1975                    .variants()
1976                    .into_iter()
1977                    .map(|variant| variant.name().to_string()),
1978            );
1979            for argument in arguments.0 {
1980                if let Some(ty) = argument.ty() {
1981                    collect_adt_constructor_names(*ty, names);
1982                }
1983            }
1984        }
1985        TyKind::RigidTy(
1986            RigidTy::FnDef(_, arguments)
1987            | RigidTy::Closure(_, arguments)
1988            | RigidTy::Coroutine(_, arguments)
1989            | RigidTy::CoroutineClosure(_, arguments)
1990            | RigidTy::CoroutineWitness(_, arguments),
1991        ) => {
1992            for argument in arguments.0 {
1993                if let Some(ty) = argument.ty() {
1994                    collect_adt_constructor_names(*ty, names);
1995                }
1996            }
1997        }
1998        TyKind::RigidTy(
1999            RigidTy::Array(ty, _)
2000            | RigidTy::Slice(ty)
2001            | RigidTy::RawPtr(ty, _)
2002            | RigidTy::Ref(_, ty, _),
2003        ) => collect_adt_constructor_names(ty, names),
2004        TyKind::RigidTy(RigidTy::Tuple(types)) => {
2005            for ty in types {
2006                collect_adt_constructor_names(ty, names);
2007            }
2008        }
2009        _ => {}
2010    }
2011}
2012
2013fn read_dyn_place(state: &DynState, place: &Place) -> DynFlow {
2014    if let Some(flow) = state.flows.get(place) {
2015        return flow.clone();
2016    }
2017    let alias_targets = resolve_alias_places(state, place);
2018    if alias_targets.len() != 1 || alias_targets.first() != Some(place) {
2019        let mut aliased = DynFlow::default();
2020        for target in alias_targets {
2021            if target != *place {
2022                aliased.merge(&read_dyn_place_without_alias(state, &target));
2023            }
2024        }
2025        if !aliased.is_empty() {
2026            return aliased;
2027        }
2028    }
2029    read_dyn_place_without_alias(state, place)
2030}
2031
2032fn read_dyn_place_without_alias(state: &DynState, place: &Place) -> DynFlow {
2033    if let Some(flow) = state.flows.get(place) {
2034        return flow.clone();
2035    }
2036    let mut ancestor = place.clone();
2037    while ancestor.projection.pop().is_some() {
2038        if let Some(flow) = state.flows.get(&ancestor) {
2039            let suffix = &place.projection[ancestor.projection.len()..];
2040            return if suffix
2041                .iter()
2042                .all(|projection| matches!(projection, ProjectionElem::Deref))
2043            {
2044                flow.clone()
2045            } else {
2046                // Locally built aggregates have explicit field entries above.
2047                // A whole-value-only region came from opaque memory (for
2048                // example an allocator-backed container), so retain its
2049                // candidates but mark the projection as partial rather than
2050                // assigning them as exact field provenance.
2051                let mut projected = flow.clone();
2052                projected
2053                    .unresolved_reasons
2054                    .insert(UnresolvedReason::ExternalMemory);
2055                projected
2056            };
2057        }
2058    }
2059    let mut result = DynFlow::default();
2060    for (candidate, flow) in &state.flows {
2061        if place_is_prefix(place, candidate) {
2062            result.merge(flow);
2063        }
2064    }
2065    result
2066}
2067
2068fn write_dyn_place(state: &mut DynState, place: &Place, flow: DynFlow) {
2069    let targets = resolve_alias_places(state, place);
2070    let targets = if targets.len() == 1 && targets[0] == *place {
2071        vec![place.clone()]
2072    } else {
2073        targets
2074    };
2075    for target in targets {
2076        state
2077            .flows
2078            .retain(|candidate, _| !place_is_prefix(&target, candidate));
2079        if !flow.is_empty() {
2080            state.flows.insert(target, flow.clone());
2081        }
2082    }
2083}
2084
2085fn merge_dyn_place(state: &mut DynState, place: &Place, flow: &DynFlow) {
2086    if flow.is_empty() {
2087        return;
2088    }
2089    let targets = resolve_alias_places(state, place);
2090    for target in targets {
2091        state.flows.entry(target).or_default().merge(flow);
2092    }
2093}
2094
2095fn resolve_alias_places(state: &DynState, place: &Place) -> Vec<Place> {
2096    if let Some(targets) = state.aliases.get(place) {
2097        return targets.clone();
2098    }
2099    let base = Place::from(place.local);
2100    let Some(targets) = state.aliases.get(&base) else {
2101        return vec![place.clone()];
2102    };
2103    let mut suffix = place.projection.as_slice();
2104    if suffix
2105        .first()
2106        .is_some_and(|projection| matches!(projection, ProjectionElem::Deref))
2107    {
2108        suffix = &suffix[1..];
2109    }
2110    targets
2111        .iter()
2112        .cloned()
2113        .map(|mut target| {
2114            target.projection.extend_from_slice(suffix);
2115            target
2116        })
2117        .collect()
2118}
2119
2120fn place_is_prefix(prefix: &Place, place: &Place) -> bool {
2121    prefix.local == place.local
2122        && prefix.projection.len() <= place.projection.len()
2123        && place.projection.starts_with(&prefix.projection)
2124}
2125
2126fn merge_dyn_state(destination: &mut Option<DynState>, source: &DynState) -> bool {
2127    let Some(destination) = destination else {
2128        *destination = Some(source.clone());
2129        return true;
2130    };
2131    let mut changed = false;
2132    for (place, flow) in &source.flows {
2133        changed |= destination
2134            .flows
2135            .entry(place.clone())
2136            .or_default()
2137            .merge(flow);
2138    }
2139    for (place, targets) in &source.aliases {
2140        let destination_targets = destination.aliases.entry(place.clone()).or_default();
2141        for target in targets {
2142            if !destination_targets.contains(target) {
2143                destination_targets.push(target.clone());
2144                changed = true;
2145            }
2146        }
2147    }
2148    changed
2149}
2150
2151fn merge_call_flow(destination: &mut Option<MirCallFlow>, source: &MirCallFlow) {
2152    let Some(destination) = destination else {
2153        *destination = Some(source.clone());
2154        return;
2155    };
2156    if destination.arguments.len() < source.arguments.len() {
2157        destination
2158            .arguments
2159            .resize_with(source.arguments.len(), DynFlow::default);
2160    }
2161    for (destination, source) in destination.arguments.iter_mut().zip(&source.arguments) {
2162        destination.merge(source);
2163    }
2164}
2165
2166fn resolve_symbolic_flow(flow: &DynFlow, arguments: &[DynFlow]) -> DynFlow {
2167    let mut resolved = DynFlow {
2168        concrete: flow.concrete.clone(),
2169        parameters: Vec::new(),
2170        unresolved_reasons: flow.unresolved_reasons.clone(),
2171    };
2172    for parameter in &flow.parameters {
2173        if let Some(argument) = arguments.get(*parameter) {
2174            resolved.merge(argument);
2175        } else {
2176            resolved
2177                .unresolved_reasons
2178                .insert(UnresolvedReason::AnalysisLimit);
2179        }
2180    }
2181    resolved
2182}
2183
2184fn specialize_semantic_functions(
2185    raw_functions: &[RawSemanticFunction],
2186    observed_vtables: &[TraitRef],
2187    trait_method_implementations: &HashMap<rustc_public::DefId, rustc_public::DefId>,
2188    crate_name: &str,
2189) -> Vec<SemanticFunction> {
2190    let index = raw_functions
2191        .iter()
2192        .enumerate()
2193        .map(|(index, function)| (function.key.clone(), index))
2194        .collect::<HashMap<_, _>>();
2195    let mut incoming = vec![0usize; raw_functions.len()];
2196    for function in raw_functions {
2197        for call in &function.calls {
2198            match &call.target {
2199                RawSemanticCallTarget::Direct { key, .. } => {
2200                    if let Some(target) = index.get(key) {
2201                        incoming[*target] += 1;
2202                    }
2203                }
2204                RawSemanticCallTarget::Dynamic { dispatch, .. } => {
2205                    for trait_ref in observed_vtables {
2206                        let Some(candidate) = resolve_dispatch_candidate(
2207                            *dispatch,
2208                            trait_ref,
2209                            trait_method_implementations,
2210                        ) else {
2211                            continue;
2212                        };
2213                        let candidate_name = stable_instance_name(candidate);
2214                        if let Some(target) = index.get(&normalize_instance_key(&candidate_name)) {
2215                            incoming[*target] += 1;
2216                        }
2217                    }
2218                }
2219                RawSemanticCallTarget::Indirect { .. } | RawSemanticCallTarget::Unresolved => {}
2220            }
2221        }
2222    }
2223
2224    let mut pending = VecDeque::<(usize, Vec<ResolvedDynFlow>)>::new();
2225    for (function, count) in incoming.iter().enumerate() {
2226        if *count == 0 {
2227            pending.push_back((function, root_dyn_context(&raw_functions[function])));
2228        }
2229    }
2230    let mut visited = HashSet::new();
2231    let mut covered_raw = HashSet::new();
2232    let mut functions = Vec::new();
2233
2234    loop {
2235        while let Some((raw_index, context)) = pending.pop_front() {
2236            let raw = &raw_functions[raw_index];
2237            let function_key = specialized_function_key(raw, &context);
2238            // Multiple compiler instances can normalize to the same display
2239            // key. Even when the emitted semantic function was already seen,
2240            // this raw instance is consumed; otherwise the disconnected-root
2241            // fallback below selects it forever.
2242            covered_raw.insert(raw_index);
2243            if !visited.insert(function_key.clone()) {
2244                continue;
2245            }
2246            let mut calls = Vec::new();
2247
2248            for raw_call in &raw.calls {
2249                let target = match &raw_call.target {
2250                    RawSemanticCallTarget::Direct { key, display } => {
2251                        let specialized = index.get(key).map_or_else(
2252                            || key.clone(),
2253                            |callee_index| {
2254                                let callee = &raw_functions[*callee_index];
2255                                let callee_context =
2256                                    call_dyn_context(callee, &raw_call.argument_flows, &context);
2257                                let key = specialized_function_key(callee, &callee_context);
2258                                pending.push_back((*callee_index, callee_context));
2259                                key
2260                            },
2261                        );
2262                        SemanticCallTarget::Direct {
2263                            key: specialized,
2264                            display: display.clone(),
2265                        }
2266                    }
2267                    RawSemanticCallTarget::Dynamic {
2268                        dispatch,
2269                        key,
2270                        display,
2271                        receiver,
2272                    } => {
2273                        let receiver_flow = semantic_dyn_flow(receiver, crate_name);
2274                        let receiver = resolve_context_flow(receiver, &context);
2275                        let mut candidates = Vec::new();
2276                        for trait_ref in &receiver.concrete {
2277                            let Some(candidate) = resolve_dispatch_candidate(
2278                                *dispatch,
2279                                trait_ref,
2280                                trait_method_implementations,
2281                            ) else {
2282                                continue;
2283                            };
2284                            let candidate_name = stable_instance_name(candidate);
2285                            let raw_candidate_key = normalize_instance_key(&candidate_name);
2286                            let candidate_key = index.get(&raw_candidate_key).map_or_else(
2287                                || raw_candidate_key.clone(),
2288                                |candidate_index| {
2289                                    let candidate_function = &raw_functions[*candidate_index];
2290                                    let candidate_context = call_dyn_context(
2291                                        candidate_function,
2292                                        &raw_call.argument_flows,
2293                                        &context,
2294                                    );
2295                                    let key = specialized_function_key(
2296                                        candidate_function,
2297                                        &candidate_context,
2298                                    );
2299                                    pending.push_back((*candidate_index, candidate_context));
2300                                    key
2301                                },
2302                            );
2303                            if !candidates
2304                                .iter()
2305                                .any(|existing: &SemanticDispatchCandidate| {
2306                                    existing.key == candidate_key
2307                                })
2308                            {
2309                                candidates.push(SemanticDispatchCandidate {
2310                                    key: candidate_key,
2311                                    display: normalize_instance_display(
2312                                        &candidate_name,
2313                                        crate_name,
2314                                    ),
2315                                });
2316                            }
2317                        }
2318                        candidates.sort_by(|left, right| left.key.cmp(&right.key));
2319                        SemanticCallTarget::Dynamic {
2320                            key: key.clone(),
2321                            display: display.clone(),
2322                            candidates,
2323                            open: !receiver.unresolved_reasons.is_empty(),
2324                            unresolved_reasons: receiver.unresolved_reasons,
2325                            receiver_flow,
2326                        }
2327                    }
2328                    RawSemanticCallTarget::Indirect { signature } => SemanticCallTarget::Indirect {
2329                        signature: signature.clone(),
2330                    },
2331                    RawSemanticCallTarget::Unresolved => SemanticCallTarget::Unresolved,
2332                };
2333                calls.push(SemanticCall {
2334                    target,
2335                    definition_name: raw_call.definition_name.clone(),
2336                    span: raw_call.span.clone(),
2337                    argument_types: raw_call.argument_types.clone(),
2338                    argument_flows: raw_call
2339                        .argument_flows
2340                        .iter()
2341                        .map(|flow| semantic_dyn_flow(flow, crate_name))
2342                        .collect(),
2343                });
2344            }
2345
2346            functions.push(SemanticFunction {
2347                base_key: raw.key.clone(),
2348                key: function_key,
2349                display: raw.display.clone(),
2350                definition_name: raw.definition_name.clone(),
2351                body_span: raw.body_span.clone(),
2352                calls,
2353                constructor_spans: raw.constructor_spans.clone(),
2354                constructor_names: raw.constructor_names.clone(),
2355                parameter_types: raw.parameter_types.clone(),
2356            });
2357        }
2358
2359        let Some(uncovered) = (0..raw_functions.len()).find(|index| !covered_raw.contains(index))
2360        else {
2361            break;
2362        };
2363        pending.push_back((uncovered, root_dyn_context(&raw_functions[uncovered])));
2364    }
2365    functions
2366}
2367
2368fn root_dyn_context(function: &RawSemanticFunction) -> Vec<ResolvedDynFlow> {
2369    function
2370        .parameter_types
2371        .iter()
2372        .map(|parameter| ResolvedDynFlow {
2373            concrete: Vec::new(),
2374            unresolved_reasons: parameter
2375                .contains("dyn ")
2376                .then_some(UnresolvedReason::OpaqueInput)
2377                .into_iter()
2378                .collect(),
2379        })
2380        .collect()
2381}
2382
2383fn call_dyn_context(
2384    callee: &RawSemanticFunction,
2385    arguments: &[DynFlow],
2386    caller_context: &[ResolvedDynFlow],
2387) -> Vec<ResolvedDynFlow> {
2388    callee
2389        .parameter_types
2390        .iter()
2391        .enumerate()
2392        .map(|(index, parameter)| {
2393            if !parameter.contains("dyn ") {
2394                return ResolvedDynFlow::default();
2395            }
2396            arguments.get(index).map_or_else(
2397                || ResolvedDynFlow {
2398                    concrete: Vec::new(),
2399                    unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
2400                },
2401                |flow| {
2402                    let mut resolved = resolve_context_flow(flow, caller_context);
2403                    if resolved.concrete.is_empty() && resolved.unresolved_reasons.is_empty() {
2404                        resolved
2405                            .unresolved_reasons
2406                            .insert(UnresolvedReason::AnalysisLimit);
2407                    }
2408                    resolved
2409                },
2410            )
2411        })
2412        .collect()
2413}
2414
2415fn resolve_context_flow(flow: &DynFlow, context: &[ResolvedDynFlow]) -> ResolvedDynFlow {
2416    let mut resolved = ResolvedDynFlow {
2417        concrete: flow.concrete.clone(),
2418        unresolved_reasons: flow.unresolved_reasons.clone(),
2419    };
2420    for parameter in &flow.parameters {
2421        if let Some(argument) = context.get(*parameter) {
2422            resolved.merge(argument);
2423        } else {
2424            resolved
2425                .unresolved_reasons
2426                .insert(UnresolvedReason::AnalysisLimit);
2427        }
2428    }
2429    resolved
2430}
2431
2432fn semantic_dyn_flow(flow: &DynFlow, crate_name: &str) -> SemanticDynFlow {
2433    let mut concrete_types = flow
2434        .concrete
2435        .iter()
2436        .map(|trait_ref| normalize_type_display(&trait_ref.self_ty().to_string(), crate_name))
2437        .collect::<Vec<_>>();
2438    concrete_types.sort();
2439    concrete_types.dedup();
2440    SemanticDynFlow {
2441        concrete_types,
2442        parameters: flow.parameters.clone(),
2443        unresolved_reasons: flow.unresolved_reasons.clone(),
2444    }
2445}
2446
2447fn specialized_function_key(function: &RawSemanticFunction, context: &[ResolvedDynFlow]) -> String {
2448    let bindings = context
2449        .iter()
2450        .enumerate()
2451        .filter(|(index, value)| {
2452            function
2453                .parameter_types
2454                .get(*index)
2455                .is_some_and(|parameter| parameter.contains("dyn "))
2456                || !value.concrete.is_empty()
2457                || !value.unresolved_reasons.is_empty()
2458        })
2459        .map(|(index, value)| {
2460            let mut candidates = value
2461                .concrete
2462                .iter()
2463                .map(|trait_ref| trait_ref.self_ty().to_string())
2464                .collect::<Vec<_>>();
2465            candidates.sort();
2466            for reason in &value.unresolved_reasons {
2467                candidates.push(format!("?{reason}"));
2468            }
2469            format!("{index}={}", candidates.join("|"))
2470        })
2471        .collect::<Vec<_>>();
2472    if bindings.is_empty() {
2473        function.key.clone()
2474    } else {
2475        format!("{}#ctx[dyn:{}]", function.key, bindings.join(","))
2476    }
2477}
2478
2479#[derive(Clone, Debug, Default, Eq, PartialEq)]
2480struct NamedDynFlow {
2481    concrete_types: BTreeSet<String>,
2482    unresolved_reasons: BTreeSet<UnresolvedReason>,
2483}
2484
2485fn respecialize_merged_semantic_program(program: &mut SemanticProgram) {
2486    if program.functions.is_empty() {
2487        return;
2488    }
2489    let mut templates = BTreeMap::<String, SemanticFunction>::new();
2490    for function in &program.functions {
2491        templates
2492            .entry(function.base_key.clone())
2493            .and_modify(|existing| {
2494                if semantic_template_score(function) > semantic_template_score(existing) {
2495                    *existing = function.clone();
2496                }
2497            })
2498            .or_insert_with(|| function.clone());
2499    }
2500    let bases = templates.keys().cloned().collect::<HashSet<_>>();
2501    let mut incoming = bases
2502        .iter()
2503        .cloned()
2504        .map(|key| (key, 0usize))
2505        .collect::<HashMap<_, _>>();
2506    for function in templates.values() {
2507        for call in &function.calls {
2508            let SemanticCallTarget::Direct { key, .. } = &call.target else {
2509                continue;
2510            };
2511            if let Some(base) = semantic_target_base(key, &bases) {
2512                *incoming.entry(base).or_default() += 1;
2513            }
2514        }
2515    }
2516
2517    let mut pending = incoming
2518        .iter()
2519        .filter(|(_, count)| **count == 0)
2520        .filter_map(|(base, _)| {
2521            templates
2522                .get(base)
2523                .map(|function| (base.clone(), root_named_context(function)))
2524        })
2525        .collect::<VecDeque<_>>();
2526    let mut visited = HashSet::new();
2527    let mut covered = HashSet::new();
2528    let mut specialized = Vec::new();
2529
2530    loop {
2531        while let Some((base, context)) = pending.pop_front() {
2532            let Some(template) = templates.get(&base) else {
2533                continue;
2534            };
2535            covered.insert(base.clone());
2536            let key = named_specialized_key(template, &context);
2537            if !visited.insert(key.clone()) {
2538                continue;
2539            }
2540            let mut function = template.clone();
2541            function.key = key;
2542            for call in &mut function.calls {
2543                match &mut call.target {
2544                    SemanticCallTarget::Direct { key, .. } => {
2545                        let Some(target_base) = semantic_target_base(key, &bases) else {
2546                            continue;
2547                        };
2548                        let Some(target) = templates.get(&target_base) else {
2549                            continue;
2550                        };
2551                        let target_context =
2552                            named_call_context(target, &call.argument_flows, &context);
2553                        *key = named_specialized_key(target, &target_context);
2554                        pending.push_back((target_base, target_context));
2555                    }
2556                    SemanticCallTarget::Dynamic {
2557                        key,
2558                        candidates,
2559                        open,
2560                        unresolved_reasons,
2561                        receiver_flow,
2562                        ..
2563                    } => {
2564                        let mut receiver = resolve_named_flow(receiver_flow, &context);
2565                        if let Some(argument_receiver) = call.argument_flows.first() {
2566                            merge_named_flow(
2567                                &mut receiver,
2568                                &resolve_named_flow(argument_receiver, &context),
2569                            );
2570                        }
2571                        *candidates = receiver
2572                            .concrete_types
2573                            .iter()
2574                            .filter_map(|concrete| {
2575                                let candidate =
2576                                    named_dispatch_candidate(concrete, key, templates.values())?;
2577                                let candidate_context =
2578                                    named_call_context(candidate, &call.argument_flows, &context);
2579                                let candidate_key =
2580                                    named_specialized_key(candidate, &candidate_context);
2581                                pending.push_back((candidate.base_key.clone(), candidate_context));
2582                                Some(SemanticDispatchCandidate {
2583                                    key: candidate_key,
2584                                    display: candidate.display.clone(),
2585                                })
2586                            })
2587                            .collect();
2588                        candidates.sort_by(|left, right| left.key.cmp(&right.key));
2589                        candidates.dedup_by(|left, right| left.key == right.key);
2590                        *unresolved_reasons = receiver.unresolved_reasons;
2591                        *open = !unresolved_reasons.is_empty();
2592                    }
2593                    SemanticCallTarget::Indirect { .. } | SemanticCallTarget::Unresolved => {}
2594                }
2595            }
2596            specialized.push(function);
2597        }
2598
2599        let Some(uncovered) = templates
2600            .keys()
2601            .find(|base| !covered.contains(*base))
2602            .cloned()
2603        else {
2604            break;
2605        };
2606        let context = root_named_context(&templates[&uncovered]);
2607        pending.push_back((uncovered, context));
2608    }
2609    specialized.sort_by(|left, right| left.key.cmp(&right.key));
2610    program.functions = specialized;
2611}
2612
2613fn merge_named_flow(destination: &mut NamedDynFlow, source: &NamedDynFlow) {
2614    destination
2615        .concrete_types
2616        .extend(source.concrete_types.iter().cloned());
2617    destination
2618        .unresolved_reasons
2619        .extend(source.unresolved_reasons.iter().cloned());
2620}
2621
2622fn semantic_template_score(function: &SemanticFunction) -> usize {
2623    let symbolic = function
2624        .calls
2625        .iter()
2626        .map(|call| {
2627            let arguments = call
2628                .argument_flows
2629                .iter()
2630                .map(|flow| flow.parameters.len())
2631                .sum::<usize>();
2632            let receiver = match &call.target {
2633                SemanticCallTarget::Dynamic { receiver_flow, .. } => receiver_flow.parameters.len(),
2634                _ => 0,
2635            };
2636            arguments + receiver
2637        })
2638        .sum::<usize>();
2639    let exact_base = usize::from(function.key == function.base_key);
2640    symbolic.saturating_mul(10) + exact_base
2641}
2642
2643fn semantic_target_base(key: &str, bases: &HashSet<String>) -> Option<String> {
2644    if bases.contains(key) {
2645        return Some(key.to_owned());
2646    }
2647    bases
2648        .iter()
2649        .filter(|base| key.starts_with(base.as_str()))
2650        .max_by_key(|base| base.len())
2651        .cloned()
2652}
2653
2654fn root_named_context(function: &SemanticFunction) -> Vec<NamedDynFlow> {
2655    function
2656        .parameter_types
2657        .iter()
2658        .map(|parameter| NamedDynFlow {
2659            concrete_types: BTreeSet::new(),
2660            unresolved_reasons: parameter
2661                .contains("dyn ")
2662                .then_some(UnresolvedReason::OpaqueInput)
2663                .into_iter()
2664                .collect(),
2665        })
2666        .collect()
2667}
2668
2669fn named_call_context(
2670    callee: &SemanticFunction,
2671    arguments: &[SemanticDynFlow],
2672    caller_context: &[NamedDynFlow],
2673) -> Vec<NamedDynFlow> {
2674    callee
2675        .parameter_types
2676        .iter()
2677        .enumerate()
2678        .map(|(index, parameter)| {
2679            if !parameter.contains("dyn ") {
2680                return NamedDynFlow::default();
2681            }
2682            arguments.get(index).map_or_else(
2683                || NamedDynFlow {
2684                    concrete_types: BTreeSet::new(),
2685                    unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
2686                },
2687                |flow| resolve_named_flow(flow, caller_context),
2688            )
2689        })
2690        .collect()
2691}
2692
2693fn resolve_named_flow(flow: &SemanticDynFlow, context: &[NamedDynFlow]) -> NamedDynFlow {
2694    let mut resolved = NamedDynFlow {
2695        concrete_types: flow.concrete_types.iter().cloned().collect(),
2696        unresolved_reasons: flow.unresolved_reasons.clone(),
2697    };
2698    for parameter in &flow.parameters {
2699        if let Some(value) = context.get(*parameter) {
2700            resolved
2701                .concrete_types
2702                .extend(value.concrete_types.iter().cloned());
2703            resolved
2704                .unresolved_reasons
2705                .extend(value.unresolved_reasons.iter().cloned());
2706        } else {
2707            resolved
2708                .unresolved_reasons
2709                .insert(UnresolvedReason::AnalysisLimit);
2710        }
2711    }
2712    resolved
2713}
2714
2715fn named_specialized_key(function: &SemanticFunction, context: &[NamedDynFlow]) -> String {
2716    let bindings = context
2717        .iter()
2718        .enumerate()
2719        .filter(|(index, flow)| {
2720            function
2721                .parameter_types
2722                .get(*index)
2723                .is_some_and(|parameter| parameter.contains("dyn "))
2724                || !flow.concrete_types.is_empty()
2725                || !flow.unresolved_reasons.is_empty()
2726        })
2727        .map(|(index, flow)| {
2728            let mut values = flow.concrete_types.iter().cloned().collect::<Vec<_>>();
2729            values.extend(
2730                flow.unresolved_reasons
2731                    .iter()
2732                    .map(|reason| format!("?{reason}")),
2733            );
2734            format!("{index}={}", values.join("|"))
2735        })
2736        .collect::<Vec<_>>();
2737    if bindings.is_empty() {
2738        function.base_key.clone()
2739    } else {
2740        format!("{}#ctx[dyn:{}]", function.base_key, bindings.join(","))
2741    }
2742}
2743
2744fn named_dispatch_candidate<'a>(
2745    concrete: &str,
2746    dispatch: &str,
2747    functions: impl Iterator<Item = &'a SemanticFunction>,
2748) -> Option<&'a SemanticFunction> {
2749    let method = dispatch
2750        .rsplit("::")
2751        .next()
2752        .unwrap_or(dispatch)
2753        .trim_end_matches('>');
2754    let concrete = concrete
2755        .rsplit("::")
2756        .next()
2757        .unwrap_or(concrete)
2758        .trim_matches(['&', ' ', '<', '>']);
2759    functions
2760        .filter(|function| {
2761            function.display.ends_with(&format!("{concrete}::{method}"))
2762                || (function.display.ends_with(&format!("::{method}"))
2763                    && function.base_key.contains(concrete)
2764                    && function.base_key.contains(" as "))
2765        })
2766        .min_by_key(|function| function.base_key.clone())
2767}
2768
2769fn merge_semantic_program(syntax: FileAnalysis, mut semantic: SemanticProgram) -> FileAnalysis {
2770    let mut analysis = FileAnalysis {
2771        source_files: syntax.source_files.clone(),
2772        ..FileAnalysis::default()
2773    };
2774    let file_identities = source_file_identities(&syntax, &semantic);
2775    canonicalize_semantic_closures(&syntax, &mut semantic, &file_identities);
2776    let local_function_keys = semantic
2777        .functions
2778        .iter()
2779        .map(|function| function.key.clone())
2780        .collect::<HashSet<_>>();
2781    let semantic_closure_keys = semantic_closure_keys(&syntax, &semantic, &file_identities);
2782    let mut required_syntax_closures = HashSet::<SymbolId>::new();
2783    let mut matched_syntax_functions = HashSet::<SymbolId>::new();
2784    let generic_syntax_functions = syntax
2785        .facts
2786        .iter()
2787        .filter(|fact| fact.kind == "generic-parameter")
2788        .map(|fact| fact.subject.clone())
2789        .collect::<HashSet<_>>();
2790    let mut syntax_functions_by_file = HashMap::<PathBuf, Vec<usize>>::new();
2791    for (index, function) in syntax.functions.iter().enumerate() {
2792        syntax_functions_by_file
2793            .entry(source_file_identity(&file_identities, &function.span.file).to_path_buf())
2794            .or_default()
2795            .push(index);
2796    }
2797    let mut facts_by_subject = HashMap::<&SymbolId, Vec<&LanguageFact>>::new();
2798    for fact in &syntax.facts {
2799        facts_by_subject
2800            .entry(&fact.subject)
2801            .or_default()
2802            .push(fact);
2803    }
2804
2805    for semantic_function in semantic.functions {
2806        let function_file =
2807            source_file_identity(&file_identities, &semantic_function.body_span.file);
2808        let Some(candidate_indices) = syntax_functions_by_file.get(function_file) else {
2809            continue;
2810        };
2811        let Some(template) = best_function_template(
2812            &syntax.functions,
2813            candidate_indices,
2814            &semantic_function.body_span,
2815        ) else {
2816            // Compiler-generated functions (drop glue, coroutine shims, etc.)
2817            // are intentionally omitted until they have explicit source syntax.
2818            continue;
2819        };
2820        matched_syntax_functions.insert(template.id.clone());
2821        let id = semantic_symbol(semantic_function.key);
2822        let mut claimed_calls = HashSet::new();
2823        let calls = template
2824            .calls
2825            .iter()
2826            .filter_map(|call| {
2827                let Some(match_index) = best_semantic_call(
2828                    call,
2829                    &semantic_function.calls,
2830                    &claimed_calls,
2831                    &file_identities,
2832                ) else {
2833                    let is_constructor = semantic_function
2834                        .constructor_spans
2835                        .iter()
2836                        .any(|span| span_coordinates_equal(&call.span, span))
2837                        || call_syntax_name(call).is_some_and(|name| {
2838                            semantic_function
2839                                .constructor_names
2840                                .iter()
2841                                .any(|constructor| constructor == name)
2842                        });
2843                    if is_constructor {
2844                        return None;
2845                    }
2846                    let mut unresolved = call.clone();
2847                    if let Some(closure) =
2848                        source_closure_for_call(template, call, &syntax.functions)
2849                    {
2850                        unresolved.target = semantic_closure_keys
2851                            .get(&closure.id)
2852                            .cloned()
2853                            .map(semantic_symbol)
2854                            .map_or_else(
2855                                || {
2856                                    required_syntax_closures.insert(closure.id.clone());
2857                                    CallTarget::Direct(closure.id.clone())
2858                                },
2859                                CallTarget::Direct,
2860                            );
2861                        unresolved.label =
2862                            replace_label_callee(&unresolved.label, closure_label_callee(closure));
2863                    }
2864                    return Some(unresolved);
2865                };
2866                let mut resolved = call.clone();
2867                claimed_calls.insert(match_index);
2868                let semantic_call = &semantic_function.calls[match_index];
2869                match &semantic_call.target {
2870                    SemanticCallTarget::Direct { key, display } => {
2871                        resolved.target = CallTarget::Direct(semantic_symbol(key.clone()));
2872                        if local_function_keys.contains(key) {
2873                            resolved.label = replace_label_callee(&resolved.label, display);
2874                        }
2875                    }
2876                    SemanticCallTarget::Dynamic {
2877                        key,
2878                        display,
2879                        candidates,
2880                        open,
2881                        unresolved_reasons,
2882                        ..
2883                    } => {
2884                        resolved.target = CallTarget::Dynamic {
2885                            dispatch: semantic_symbol(key.clone()),
2886                            candidates: candidates
2887                                .iter()
2888                                .map(|candidate| DispatchCandidate {
2889                                    target: semantic_symbol(candidate.key.clone()),
2890                                    label: replace_label_callee(
2891                                        &resolved.label,
2892                                        &candidate.display,
2893                                    ),
2894                                })
2895                                .collect(),
2896                            resolution: if candidates.is_empty() {
2897                                DispatchResolution::Unresolved
2898                            } else if *open {
2899                                DispatchResolution::Partial
2900                            } else {
2901                                DispatchResolution::Complete
2902                            },
2903                            evidence: DispatchEvidence::ExactFlow,
2904                            unresolved_reasons: if unresolved_reasons.is_empty()
2905                                && candidates.is_empty()
2906                            {
2907                                [UnresolvedReason::AnalysisLimit].into_iter().collect()
2908                            } else {
2909                                unresolved_reasons.clone()
2910                            },
2911                        };
2912                        resolved.label = replace_label_callee(&resolved.label, display);
2913                    }
2914                    SemanticCallTarget::Indirect { signature } => {
2915                        resolved.target = CallTarget::Indirect {
2916                            signature: Some(signature.clone()),
2917                            reason: UnresolvedReason::FunctionPointer,
2918                        };
2919                    }
2920                    SemanticCallTarget::Unresolved => {
2921                        resolved.target = CallTarget::Unresolved;
2922                    }
2923                }
2924                let argument_types = semantic_call_argument_types(semantic_call);
2925                resolved.label.typed = Some(annotate_rust_label(
2926                    &resolved.label.default,
2927                    &argument_types,
2928                ));
2929                Some(resolved)
2930            })
2931            .collect();
2932
2933        let mut function_label = replace_label_callee(&template.label, &semantic_function.display);
2934        function_label.typed = Some(annotate_rust_label(
2935            &function_label.default,
2936            &semantic_function.parameter_types,
2937        ));
2938        analysis.functions.push(FunctionInfo {
2939            id: id.clone(),
2940            label: function_label,
2941            public: template.public,
2942            calls,
2943            span: template.span.clone(),
2944        });
2945        if let Some(facts) = facts_by_subject.get(&template.id) {
2946            analysis.facts.extend(facts.iter().map(|fact| {
2947                let mut fact = (*fact).clone();
2948                fact.subject = id.clone();
2949                fact
2950            }));
2951        }
2952    }
2953
2954    for function in &syntax.functions {
2955        let preserve_closure = required_syntax_closures.contains(&function.id);
2956        let preserve_open_generic = generic_syntax_functions.contains(&function.id)
2957            && !matched_syntax_functions.contains(&function.id);
2958        if !preserve_closure && !preserve_open_generic {
2959            continue;
2960        }
2961        analysis.functions.push(function.clone());
2962        if preserve_open_generic && let Some(facts) = facts_by_subject.get(&function.id) {
2963            analysis.facts.extend(facts.iter().copied().cloned());
2964        }
2965    }
2966
2967    synthesize_referenced_generic_instances(&mut analysis, &syntax);
2968
2969    analysis
2970}
2971
2972fn synthesize_referenced_generic_instances(analysis: &mut FileAnalysis, syntax: &FileAnalysis) {
2973    let generic_parameters = syntax
2974        .facts
2975        .iter()
2976        .filter(|fact| fact.kind == "generic-parameter")
2977        .fold(BTreeMap::<SymbolId, Vec<String>>::new(), |mut map, fact| {
2978            map.entry(fact.subject.clone())
2979                .or_default()
2980                .push(fact.key.clone());
2981            map
2982        });
2983    if generic_parameters.is_empty() {
2984        return;
2985    }
2986
2987    let mut used_templates = HashSet::new();
2988    loop {
2989        let known = analysis
2990            .functions
2991            .iter()
2992            .map(|function| function.id.clone())
2993            .collect::<HashSet<_>>();
2994        let missing = analysis
2995            .functions
2996            .iter()
2997            .flat_map(|function| &function.calls)
2998            .filter_map(|call| match &call.target {
2999                CallTarget::Direct(target)
3000                    if !known.contains(target) && target.language.0 == "rust" =>
3001                {
3002                    Some(target.clone())
3003                }
3004                _ => None,
3005            })
3006            .collect::<BTreeSet<_>>();
3007        let mut added = Vec::new();
3008        for target in missing {
3009            let Some((base, arguments)) = generic_instance_parts(&target.name) else {
3010                continue;
3011            };
3012            let Some(template) = syntax.functions.iter().find(|function| {
3013                function.id.name == base
3014                    && generic_parameters
3015                        .get(&function.id)
3016                        .is_some_and(|parameters| parameters.len() == arguments.len())
3017            }) else {
3018                continue;
3019            };
3020            let parameters = &generic_parameters[&template.id];
3021            let bindings = parameters
3022                .iter()
3023                .cloned()
3024                .zip(arguments.iter().cloned())
3025                .collect::<HashMap<_, _>>();
3026            let mut function = template.clone();
3027            function.id = target.clone();
3028            let display = normalize_instance_display(&target.name, "");
3029            function.label = replace_label_callee(&function.label, &display);
3030            for call in &mut function.calls {
3031                substitute_generic_call(call, &bindings, &analysis.functions);
3032            }
3033            used_templates.insert(template.id.clone());
3034            added.push(function);
3035        }
3036        if added.is_empty() {
3037            break;
3038        }
3039        analysis.functions.extend(added);
3040    }
3041
3042    let displays = analysis
3043        .functions
3044        .iter()
3045        .map(|function| {
3046            (
3047                function.id.clone(),
3048                outer_call_arguments_start(&function.label.default)
3049                    .map_or(function.label.default.clone(), |start| {
3050                        function.label.default[..start].to_owned()
3051                    }),
3052            )
3053        })
3054        .collect::<HashMap<_, _>>();
3055    for function in &mut analysis.functions {
3056        for call in &mut function.calls {
3057            let CallTarget::Direct(target) = &call.target else {
3058                continue;
3059            };
3060            if let Some(display) = displays.get(target) {
3061                call.label = replace_label_callee(&call.label, display);
3062            }
3063        }
3064    }
3065    analysis
3066        .functions
3067        .retain(|function| !used_templates.contains(&function.id));
3068    analysis
3069        .facts
3070        .retain(|fact| !used_templates.contains(&fact.subject));
3071}
3072
3073fn generic_instance_parts(instance: &str) -> Option<(String, Vec<String>)> {
3074    let start = instance.find('<')?;
3075    let end = instance.rfind('>')?;
3076    if end <= start {
3077        return None;
3078    }
3079    let base = instance[..start]
3080        .rsplit("::")
3081        .next()
3082        .unwrap_or(&instance[..start])
3083        .to_owned();
3084    let arguments = split_rust_arguments(&instance[start + 1..end])
3085        .into_iter()
3086        .map(simplify_type_paths)
3087        .collect();
3088    Some((base, arguments))
3089}
3090
3091fn substitute_generic_call(
3092    call: &mut CallSite,
3093    bindings: &HashMap<String, String>,
3094    functions: &[FunctionInfo],
3095) {
3096    let CallTarget::Direct(target) = &call.target else {
3097        return;
3098    };
3099    let Some(container) = target.container.as_deref() else {
3100        return;
3101    };
3102    let parameter = container
3103        .split_once(" as ")
3104        .map_or(container, |(parameter, _)| parameter);
3105    let Some(concrete) = bindings.get(parameter) else {
3106        return;
3107    };
3108    let callee = format!("{concrete}::{}", target.name);
3109    call.label = replace_label_callee(&call.label, &callee);
3110    call.target = functions
3111        .iter()
3112        .find(|function| {
3113            outer_call_arguments_start(&function.label.default)
3114                .is_some_and(|start| function.label.default[..start] == callee)
3115        })
3116        .map(|function| function.id.clone())
3117        .map_or_else(
3118            || {
3119                CallTarget::Direct(SymbolId {
3120                    language: LanguageId::new("rust"),
3121                    module: target.module.clone(),
3122                    container: Some(concrete.clone()),
3123                    name: target.name.clone(),
3124                })
3125            },
3126            CallTarget::Direct,
3127        );
3128}
3129
3130fn semantic_closure_keys(
3131    syntax: &FileAnalysis,
3132    semantic: &SemanticProgram,
3133    file_identities: &HashMap<PathBuf, PathBuf>,
3134) -> HashMap<SymbolId, String> {
3135    syntax
3136        .functions
3137        .iter()
3138        .filter(|function| is_source_closure(function))
3139        .filter_map(|closure| {
3140            semantic
3141                .functions
3142                .iter()
3143                .filter(|function| {
3144                    source_file_identity(file_identities, &closure.span.file)
3145                        == source_file_identity(file_identities, &function.body_span.file)
3146                        && span_contains(&closure.span, &function.body_span)
3147                })
3148                .min_by_key(|function| span_size(&function.body_span))
3149                .map(|function| (closure.id.clone(), function.key.clone()))
3150        })
3151        .collect()
3152}
3153
3154fn source_closure_for_call<'a>(
3155    owner: &FunctionInfo,
3156    call: &CallSite,
3157    functions: &'a [FunctionInfo],
3158) -> Option<&'a FunctionInfo> {
3159    let name = call_syntax_name(call)?;
3160    let expected_container = owner.id.qualified_parts().join("::");
3161    let expected_lambda = format!("λ{name}");
3162    functions.iter().find(|function| {
3163        is_source_closure(function)
3164            && function.id.container.as_deref() == Some(expected_container.as_str())
3165            && closure_label_callee(function) == expected_lambda
3166    })
3167}
3168
3169fn closure_label_callee(function: &FunctionInfo) -> &str {
3170    outer_call_arguments_start(&function.label.default)
3171        .map_or(function.label.default.as_str(), |start| {
3172            &function.label.default[..start]
3173        })
3174}
3175
3176#[derive(Clone)]
3177struct SemanticClosureSource {
3178    span: SourceSpan,
3179    identity: String,
3180    lambda: String,
3181}
3182
3183#[derive(Clone, Copy)]
3184enum ClosureRewrite {
3185    Identity,
3186    Display,
3187}
3188
3189fn canonicalize_semantic_closures(
3190    syntax: &FileAnalysis,
3191    semantic: &mut SemanticProgram,
3192    file_identities: &HashMap<PathBuf, PathBuf>,
3193) {
3194    let closures = syntax
3195        .functions
3196        .iter()
3197        .filter(|function| is_source_closure(function))
3198        .map(|function| SemanticClosureSource {
3199            span: function.span.clone(),
3200            identity: format!("{{lambda-id:{}}}", function.id),
3201            lambda: outer_call_arguments_start(&function.label.default).map_or_else(
3202                || function.label.default.clone(),
3203                |start| function.label.default[..start].to_owned(),
3204            ),
3205        })
3206        .collect::<Vec<_>>();
3207    let compiler_sources = semantic
3208        .functions
3209        .iter()
3210        .filter_map(|function| {
3211            let source = closure_for_body_span(&function.body_span, &closures, file_identities)?;
3212            let marker = compiler_closure_marker(&function.definition_name)?;
3213            Some((marker, (source.identity.clone(), source.lambda.clone())))
3214        })
3215        .collect::<HashMap<_, _>>();
3216
3217    let mut direct_closures = HashMap::<String, (String, String)>::new();
3218    for function in &semantic.functions {
3219        let Some(source) = closure_for_body_span(&function.body_span, &closures, file_identities)
3220        else {
3221            continue;
3222        };
3223        let located_key = canonicalize_closure_value(
3224            &function.key,
3225            &closures,
3226            file_identities,
3227            ClosureRewrite::Identity,
3228            &compiler_sources,
3229        );
3230        let key = replace_closure_definition(&located_key, &source.identity);
3231        let display = canonicalize_closure_value(
3232            &closure_instance_display(&source.lambda, &function.display),
3233            &closures,
3234            file_identities,
3235            ClosureRewrite::Display,
3236            &compiler_sources,
3237        );
3238        direct_closures.insert(function.key.clone(), (key, display));
3239    }
3240
3241    for function in &mut semantic.functions {
3242        let original_key = function.key.clone();
3243        if let Some((key, display)) = direct_closures.get(&original_key) {
3244            function.key = key.clone();
3245            function.display = display.clone();
3246        } else {
3247            function.key = canonicalize_closure_value(
3248                &function.key,
3249                &closures,
3250                file_identities,
3251                ClosureRewrite::Identity,
3252                &compiler_sources,
3253            );
3254            function.display = canonicalize_closure_value(
3255                &function.display,
3256                &closures,
3257                file_identities,
3258                ClosureRewrite::Display,
3259                &compiler_sources,
3260            );
3261        }
3262        for parameter in &mut function.parameter_types {
3263            *parameter = canonicalize_closure_value(
3264                parameter,
3265                &closures,
3266                file_identities,
3267                ClosureRewrite::Display,
3268                &compiler_sources,
3269            );
3270        }
3271        for call in &mut function.calls {
3272            for argument in &mut call.argument_types {
3273                *argument = canonicalize_closure_value(
3274                    argument,
3275                    &closures,
3276                    file_identities,
3277                    ClosureRewrite::Display,
3278                    &compiler_sources,
3279                );
3280            }
3281            match &mut call.target {
3282                SemanticCallTarget::Direct { key, display } => {
3283                    rewrite_closure_target(
3284                        key,
3285                        display,
3286                        &direct_closures,
3287                        &closures,
3288                        file_identities,
3289                        &compiler_sources,
3290                    );
3291                }
3292                SemanticCallTarget::Dynamic {
3293                    key,
3294                    display,
3295                    candidates,
3296                    ..
3297                } => {
3298                    rewrite_closure_target(
3299                        key,
3300                        display,
3301                        &direct_closures,
3302                        &closures,
3303                        file_identities,
3304                        &compiler_sources,
3305                    );
3306                    for candidate in candidates {
3307                        rewrite_closure_target(
3308                            &mut candidate.key,
3309                            &mut candidate.display,
3310                            &direct_closures,
3311                            &closures,
3312                            file_identities,
3313                            &compiler_sources,
3314                        );
3315                    }
3316                }
3317                SemanticCallTarget::Indirect { .. } => {}
3318                SemanticCallTarget::Unresolved => {}
3319            }
3320        }
3321    }
3322}
3323
3324fn is_source_closure(function: &FunctionInfo) -> bool {
3325    function.id.name.starts_with("{lambda:")
3326}
3327
3328fn rewrite_closure_target(
3329    key: &mut String,
3330    display: &mut String,
3331    direct_closures: &HashMap<String, (String, String)>,
3332    closures: &[SemanticClosureSource],
3333    file_identities: &HashMap<PathBuf, PathBuf>,
3334    compiler_sources: &HashMap<String, (String, String)>,
3335) {
3336    if let Some((canonical_key, canonical_display)) = direct_closures.get(key) {
3337        *key = canonical_key.clone();
3338        *display = canonical_display.clone();
3339        return;
3340    }
3341    *key = canonicalize_closure_value(
3342        key,
3343        closures,
3344        file_identities,
3345        ClosureRewrite::Identity,
3346        compiler_sources,
3347    );
3348    *display = canonicalize_closure_value(
3349        display,
3350        closures,
3351        file_identities,
3352        ClosureRewrite::Display,
3353        compiler_sources,
3354    );
3355}
3356
3357fn canonicalize_closure_value(
3358    value: &str,
3359    closures: &[SemanticClosureSource],
3360    file_identities: &HashMap<PathBuf, PathBuf>,
3361    rewrite: ClosureRewrite,
3362    compiler_sources: &HashMap<String, (String, String)>,
3363) -> String {
3364    let mut value = rewrite_located_closures(value, closures, file_identities, rewrite);
3365    for (marker, (identity, display)) in compiler_sources {
3366        value = value.replace(
3367            marker,
3368            match rewrite {
3369                ClosureRewrite::Identity => identity,
3370                ClosureRewrite::Display => display,
3371            },
3372        );
3373    }
3374    match rewrite {
3375        ClosureRewrite::Identity => value,
3376        ClosureRewrite::Display => render_compiler_closure_markers(&value),
3377    }
3378}
3379
3380fn render_compiler_closure_markers(value: &str) -> String {
3381    let mut output = String::with_capacity(value.len());
3382    let mut remainder = value;
3383    while let Some(start) = remainder.find("{lambda-def:") {
3384        output.push_str(&remainder[..start]);
3385        let marker = &remainder[start..];
3386        let Some(end) = marker.find('}') else {
3387            output.push_str(marker);
3388            return output;
3389        };
3390        let label = marker[..end]
3391            .rsplit_once(':')
3392            .map_or(&marker[..=end], |(_, label)| label);
3393        output.push_str(label);
3394        remainder = &marker[end + 1..];
3395    }
3396    output.push_str(remainder);
3397    output
3398}
3399
3400fn closure_for_body_span<'a>(
3401    body: &SourceSpan,
3402    closures: &'a [SemanticClosureSource],
3403    file_identities: &HashMap<PathBuf, PathBuf>,
3404) -> Option<&'a SemanticClosureSource> {
3405    closures
3406        .iter()
3407        .filter(|closure| {
3408            source_file_identity(file_identities, &closure.span.file)
3409                == source_file_identity(file_identities, &body.file)
3410                && span_contains(&closure.span, body)
3411        })
3412        .min_by_key(|closure| span_size(&closure.span))
3413}
3414
3415fn rewrite_located_closures(
3416    value: &str,
3417    closures: &[SemanticClosureSource],
3418    file_identities: &HashMap<PathBuf, PathBuf>,
3419    rewrite: ClosureRewrite,
3420) -> String {
3421    let mut output = String::with_capacity(value.len());
3422    let mut remainder = value;
3423    loop {
3424        let located = ["{closure@", "{async closure@"]
3425            .into_iter()
3426            .filter_map(|prefix| remainder.find(prefix).map(|start| (start, prefix)))
3427            .min_by_key(|(start, _)| *start);
3428        let Some((start, prefix)) = located else {
3429            output.push_str(remainder);
3430            break;
3431        };
3432        output.push_str(&remainder[..start]);
3433        let located = &remainder[start..];
3434        let Some(end) = located.find('}') else {
3435            output.push_str(located);
3436            break;
3437        };
3438        let location = &located[prefix.len()..end];
3439        let replacement = parse_anonymous_location(location).and_then(|(file, line, column)| {
3440            closure_for_location(&file, line, column, closures, file_identities)
3441        });
3442        if let Some(closure) = replacement {
3443            output.push_str(match rewrite {
3444                ClosureRewrite::Identity => &closure.identity,
3445                ClosureRewrite::Display => &closure.lambda,
3446            });
3447        } else {
3448            output.push_str(&located[..=end]);
3449        }
3450        remainder = &located[end + 1..];
3451    }
3452    output
3453}
3454
3455fn parse_anonymous_location(location: &str) -> Option<(PathBuf, usize, usize)> {
3456    let mut fields = location.rsplitn(5, ':');
3457    let _end_column = fields.next()?.trim().parse::<usize>().ok()?;
3458    let _end_line = fields.next()?.trim().parse::<usize>().ok()?;
3459    let start_column = fields.next()?.trim().parse::<usize>().ok()?;
3460    let start_line = fields.next()?.trim().parse::<usize>().ok()?;
3461    let file = PathBuf::from(fields.next()?.trim());
3462    Some((file, start_line, start_column))
3463}
3464
3465fn closure_for_location<'a>(
3466    file: &Path,
3467    line: usize,
3468    column: usize,
3469    closures: &'a [SemanticClosureSource],
3470    file_identities: &HashMap<PathBuf, PathBuf>,
3471) -> Option<&'a SemanticClosureSource> {
3472    let candidates = closures
3473        .iter()
3474        .filter(|closure| {
3475            source_paths_match(file_identities, &closure.span.file, file)
3476                && closure.span.start_line == line
3477        })
3478        .collect::<Vec<_>>();
3479    let best_distance = candidates
3480        .iter()
3481        .map(|closure| closure.span.start_column.saturating_add(1).abs_diff(column))
3482        .min()?;
3483    let mut best = candidates.into_iter().filter(|closure| {
3484        closure.span.start_column.saturating_add(1).abs_diff(column) == best_distance
3485    });
3486    let closure = best.next()?;
3487    best.next().is_none().then_some(closure)
3488}
3489
3490fn replace_closure_definition(value: &str, replacement: &str) -> String {
3491    let Some(start) = ["{closure#", "{async closure#"]
3492        .into_iter()
3493        .filter_map(|prefix| value.rfind(prefix))
3494        .max()
3495    else {
3496        return value.to_owned();
3497    };
3498    let Some(end) = value[start..].find('}').map(|end| start + end) else {
3499        return value.to_owned();
3500    };
3501    format!("{}{replacement}{}", &value[..start], &value[end + 1..])
3502}
3503
3504fn closure_instance_display(lambda: &str, semantic: &str) -> String {
3505    let Some(start) = ["::{closure#", "::{async closure#"]
3506        .into_iter()
3507        .filter_map(|prefix| semantic.rfind(prefix))
3508        .max()
3509    else {
3510        return lambda.to_owned();
3511    };
3512    let parent = &semantic[..start];
3513    let Some(generic_start) = trailing_generic_arguments_start(parent) else {
3514        return lambda.to_owned();
3515    };
3516    format!("{}{}", lambda, &parent[generic_start..])
3517}
3518
3519fn source_file_identities(
3520    syntax: &FileAnalysis,
3521    semantic: &SemanticProgram,
3522) -> HashMap<PathBuf, PathBuf> {
3523    let mut paths = HashSet::<PathBuf>::new();
3524    for function in &syntax.functions {
3525        paths.insert(function.span.file.clone());
3526        paths.extend(function.calls.iter().map(|call| call.span.file.clone()));
3527    }
3528    for function in &semantic.functions {
3529        paths.insert(function.body_span.file.clone());
3530        paths.extend(function.calls.iter().map(|call| call.span.file.clone()));
3531    }
3532
3533    paths
3534        .into_iter()
3535        .map(|path| {
3536            let identity = path.canonicalize().unwrap_or_else(|_| {
3537                if path.is_absolute() {
3538                    path.clone()
3539                } else {
3540                    env::current_dir().map_or_else(|_| path.clone(), |root| root.join(&path))
3541                }
3542            });
3543            (path, identity)
3544        })
3545        .collect()
3546}
3547
3548fn source_file_identity<'a>(identities: &'a HashMap<PathBuf, PathBuf>, path: &'a Path) -> &'a Path {
3549    identities.get(path).map_or(path, PathBuf::as_path)
3550}
3551
3552fn source_paths_match(identities: &HashMap<PathBuf, PathBuf>, left: &Path, right: &Path) -> bool {
3553    let left = source_file_identity(identities, left);
3554    let right = source_file_identity(identities, right);
3555    left == right
3556        || (left.is_absolute() && !right.is_absolute() && left.ends_with(right))
3557        || (right.is_absolute() && !left.is_absolute() && right.ends_with(left))
3558}
3559
3560fn best_function_template<'a>(
3561    functions: &'a [FunctionInfo],
3562    candidate_indices: &[usize],
3563    body_span: &SourceSpan,
3564) -> Option<&'a FunctionInfo> {
3565    candidate_indices
3566        .iter()
3567        .filter_map(|index| functions.get(*index))
3568        .filter(|function| span_contains(&function.span, body_span))
3569        .min_by_key(|function| span_size(&function.span))
3570}
3571
3572fn best_semantic_call(
3573    syntax_call: &CallSite,
3574    semantic_calls: &[SemanticCall],
3575    claimed: &HashSet<usize>,
3576    file_identities: &HashMap<PathBuf, PathBuf>,
3577) -> Option<usize> {
3578    let syntax_name = call_syntax_name(syntax_call)?;
3579
3580    semantic_calls
3581        .iter()
3582        .enumerate()
3583        .filter(|(index, call)| {
3584            if claimed.contains(index)
3585                || source_file_identity(file_identities, &syntax_call.span.file)
3586                    != source_file_identity(file_identities, &call.span.file)
3587                || !spans_overlap(&syntax_call.span, &call.span)
3588            {
3589                return false;
3590            }
3591            let definition_leaf = call
3592                .definition_name
3593                .rsplit("::")
3594                .next()
3595                .unwrap_or(&call.definition_name);
3596            definition_leaf == syntax_name || span_coordinates_equal(&syntax_call.span, &call.span)
3597        })
3598        .min_by_key(|(_, call)| {
3599            let definition_leaf = call
3600                .definition_name
3601                .rsplit("::")
3602                .next()
3603                .unwrap_or(&call.definition_name);
3604            let name_penalty = usize::from(definition_leaf != syntax_name) * 1_000_000;
3605            name_penalty + span_distance(&syntax_call.span, &call.span)
3606        })
3607        .map(|(index, _)| index)
3608}
3609
3610fn call_syntax_name(call: &CallSite) -> Option<&str> {
3611    match &call.syntax {
3612        CallSyntax::Path(parts) => parts.last().map(String::as_str),
3613        CallSyntax::SelfMethod(method) | CallSyntax::Method { method, .. } => Some(method.as_str()),
3614    }
3615}
3616
3617fn span_coordinates_equal(left: &SourceSpan, right: &SourceSpan) -> bool {
3618    left.start_line == right.start_line
3619        && left.start_column == right.start_column
3620        && left.end_line == right.end_line
3621        && left.end_column == right.end_column
3622}
3623
3624fn semantic_symbol(name: String) -> SymbolId {
3625    SymbolId {
3626        language: LanguageId::new("rust"),
3627        module: Vec::new(),
3628        container: None,
3629        name,
3630    }
3631}
3632
3633fn stable_instance_name(instance: Instance) -> String {
3634    let name = instance.name().to_string();
3635    let mut replacements = BTreeMap::<String, Option<String>>::new();
3636    collect_compiler_closure_replacements(&instance.args(), &mut replacements);
3637    replacements
3638        .into_iter()
3639        .filter_map(|(located, marker)| marker.map(|marker| (located, marker)))
3640        .fold(name, |name, (located, marker)| {
3641            name.replace(&located, &marker)
3642        })
3643}
3644
3645fn collect_compiler_closure_replacements(
3646    arguments: &GenericArgs,
3647    replacements: &mut BTreeMap<String, Option<String>>,
3648) {
3649    for argument in &arguments.0 {
3650        if let GenericArgKind::Type(ty) = argument {
3651            collect_direct_closure_replacement(*ty, replacements);
3652        }
3653    }
3654}
3655
3656fn collect_direct_closure_replacement(ty: Ty, replacements: &mut BTreeMap<String, Option<String>>) {
3657    // `rustc_public` still has unimplemented conversions for aliases and
3658    // coroutine closures. Inspecting every nested `TyKind` can therefore panic
3659    // on otherwise valid crates (Rayon contains such aliases). Ordinary
3660    // closures passed as function generics are direct arguments here, so only
3661    // open that known-safe shape.
3662    let rendered = ty.to_string();
3663    if !rendered.starts_with("{closure@") {
3664        return;
3665    }
3666    let TyKind::RigidTy(RigidTy::Closure(definition, _)) = ty.kind() else {
3667        return;
3668    };
3669    record_compiler_closure(&rendered, &definition.name().to_string(), replacements);
3670}
3671
3672fn record_compiler_closure(
3673    rendered: &str,
3674    definition_name: &str,
3675    replacements: &mut BTreeMap<String, Option<String>>,
3676) {
3677    let Some(located) = located_closure_token(rendered) else {
3678        return;
3679    };
3680    let Some(marker) = compiler_closure_marker(definition_name) else {
3681        return;
3682    };
3683    replacements
3684        .entry(located.to_owned())
3685        .and_modify(|current| {
3686            if current.as_deref() != Some(&marker) {
3687                *current = None;
3688            }
3689        })
3690        .or_insert(Some(marker));
3691}
3692
3693fn located_closure_token(value: &str) -> Option<&str> {
3694    let start = ["{closure@", "{async closure@"]
3695        .into_iter()
3696        .filter_map(|prefix| value.find(prefix))
3697        .min()?;
3698    let end = value[start..].find('}')? + start;
3699    Some(&value[start..=end])
3700}
3701
3702fn compiler_closure_marker(definition_name: &str) -> Option<String> {
3703    let label = compiler_closure_label(definition_name)?;
3704    let mut hasher = Sha256::new();
3705    hasher.update(definition_name.as_bytes());
3706    let identity = format!("{:x}", hasher.finalize());
3707    Some(format!("{{lambda-def:{}:{label}}}", &identity[..12]))
3708}
3709
3710fn compiler_closure_label(definition_name: &str) -> Option<String> {
3711    let (parent, ordinal) = ["::{closure#", "::{async closure#"]
3712        .into_iter()
3713        .find_map(|separator| definition_name.rsplit_once(separator))?;
3714    let ordinal = ordinal.strip_suffix('}')?.parse::<usize>().ok()? + 1;
3715    let parent = parent.rsplit("::").next().unwrap_or("closure");
3716    let parent = parent
3717        .chars()
3718        .map(|character| {
3719            if character.is_ascii_alphanumeric() || character == '_' {
3720                character
3721            } else {
3722                '_'
3723            }
3724        })
3725        .collect::<String>();
3726    let parent = if parent.trim_matches('_').is_empty() {
3727        "closure"
3728    } else {
3729        parent.trim_matches('_')
3730    };
3731    Some(format!("λ{parent}#{ordinal}"))
3732}
3733
3734fn normalize_instance_key(name: &str) -> String {
3735    normalize_anonymous_type_locations(&name.replace("::<", "<"))
3736}
3737
3738fn normalize_instance_display(name: &str, crate_name: &str) -> String {
3739    let without_crate = if crate_name.is_empty() {
3740        name.to_owned()
3741    } else {
3742        name.replace(&format!("{crate_name}::"), "")
3743    };
3744    let compact = normalize_anonymous_type_locations(&without_crate.replace("::<", "<"));
3745    collapse_trait_qualification(&compact)
3746}
3747
3748fn normalize_anonymous_type_locations(value: &str) -> String {
3749    [
3750        ("{async block@", "{async block}"),
3751        ("{coroutine@", "{coroutine}"),
3752        ("{generator@", "{generator}"),
3753    ]
3754    .into_iter()
3755    .fold(value.to_owned(), |value, (prefix, replacement)| {
3756        replace_braced_location(&value, prefix, replacement)
3757    })
3758}
3759
3760fn replace_braced_location(value: &str, prefix: &str, replacement: &str) -> String {
3761    let mut output = String::with_capacity(value.len());
3762    let mut remainder = value;
3763    while let Some(start) = remainder.find(prefix) {
3764        output.push_str(&remainder[..start]);
3765        let located = &remainder[start..];
3766        let Some(end) = located.find('}') else {
3767            output.push_str(located);
3768            return output;
3769        };
3770        output.push_str(replacement);
3771        remainder = &located[end + 1..];
3772    }
3773    output.push_str(remainder);
3774    output
3775}
3776
3777fn collapse_trait_qualification(name: &str) -> String {
3778    let Some(rest) = name.strip_prefix('<') else {
3779        return name.to_owned();
3780    };
3781    let Some((qualification, method)) = rest.rsplit_once(">::") else {
3782        return name.to_owned();
3783    };
3784    let Some((self_type, _trait_name)) = qualification.rsplit_once(" as ") else {
3785        return name.to_owned();
3786    };
3787    format!("{self_type}::{method}")
3788}
3789
3790fn replace_label_callee(label: &CallLabel, callee: &str) -> CallLabel {
3791    CallLabel {
3792        default: replace_callee_text(&label.default, callee),
3793        typed: label
3794            .typed
3795            .as_ref()
3796            .map(|typed| replace_callee_text(typed, callee)),
3797    }
3798}
3799
3800fn replace_callee_text(label: &str, callee: &str) -> String {
3801    if let Some(arguments_start) = outer_call_arguments_start(label) {
3802        format!(
3803            "{}{}",
3804            semantic_callee(&label[..arguments_start], callee),
3805            &label[arguments_start..]
3806        )
3807    } else {
3808        semantic_callee(label, callee)
3809    }
3810}
3811
3812fn semantic_callee(source: &str, semantic: &str) -> String {
3813    if semantic.starts_with('λ') {
3814        return semantic.to_owned();
3815    }
3816    if semantic.starts_with("dyn ") {
3817        return semantic.rsplit_once("::").map_or_else(
3818            || semantic.to_owned(),
3819            |(dispatch, method)| format!("{}::{method}", simplify_type_paths(dispatch)),
3820        );
3821    }
3822
3823    if semantic.contains('<') {
3824        let source_segments = split_top_level_path(source);
3825        let semantic_segments = split_top_level_path(semantic);
3826        let keep = source_segments.len().min(semantic_segments.len()).max(1);
3827        return semantic_segments[semantic_segments.len().saturating_sub(keep)..]
3828            .iter()
3829            .map(|segment| simplify_type_paths(segment))
3830            .collect::<Vec<_>>()
3831            .join("::");
3832    }
3833
3834    let parts = semantic.split("::").collect::<Vec<_>>();
3835    if parts.len() >= 2
3836        && parts[parts.len() - 2]
3837            .chars()
3838            .next()
3839            .is_some_and(char::is_uppercase)
3840    {
3841        return format!("{}::{}", parts[parts.len() - 2], parts[parts.len() - 1]);
3842    }
3843    source.to_owned()
3844}
3845
3846fn split_top_level_path(value: &str) -> Vec<&str> {
3847    let mut segments = Vec::new();
3848    let mut depth = 0usize;
3849    let mut start = 0usize;
3850    let bytes = value.as_bytes();
3851    let mut index = 0usize;
3852    while index < bytes.len() {
3853        match bytes[index] {
3854            b'<' => depth += 1,
3855            b'>' => depth = depth.saturating_sub(1),
3856            b':' if depth == 0 && bytes.get(index + 1) == Some(&b':') => {
3857                segments.push(&value[start..index]);
3858                index += 1;
3859                start = index + 1;
3860            }
3861            _ => {}
3862        }
3863        index += 1;
3864    }
3865    segments.push(&value[start..]);
3866    segments
3867}
3868
3869fn simplify_type_paths(value: &str) -> String {
3870    let mut output = String::with_capacity(value.len());
3871    let mut token = String::new();
3872    let flush = |token: &mut String, output: &mut String| {
3873        if token.is_empty() {
3874            return;
3875        }
3876        output.push_str(token.rsplit("::").next().unwrap_or(token));
3877        token.clear();
3878    };
3879    for character in value.chars() {
3880        if character.is_ascii_alphanumeric() || matches!(character, '_' | ':' | '\'') {
3881            token.push(character);
3882        } else {
3883            flush(&mut token, &mut output);
3884            output.push(character);
3885        }
3886    }
3887    flush(&mut token, &mut output);
3888    output
3889}
3890
3891fn normalize_type_display(value: &str, crate_name: &str) -> String {
3892    simplify_type_paths(&normalize_anonymous_type_locations(
3893        &value.replace(&format!("{crate_name}::"), ""),
3894    ))
3895}
3896
3897fn semantic_call_argument_types(call: &SemanticCall) -> Vec<String> {
3898    let is_closure = matches!(
3899        &call.target,
3900        SemanticCallTarget::Direct { display, .. }
3901            if display.starts_with('λ') || display.contains("{closure#")
3902    );
3903    if !is_closure {
3904        return call.argument_types.clone();
3905    }
3906    let Some(tuple) = call.argument_types.last() else {
3907        return Vec::new();
3908    };
3909    let Some(tuple) = tuple
3910        .strip_prefix('(')
3911        .and_then(|tuple| tuple.strip_suffix(')'))
3912    else {
3913        return call.argument_types.clone();
3914    };
3915    split_rust_arguments(tuple)
3916        .into_iter()
3917        .filter(|argument| !argument.is_empty())
3918        .map(ToOwned::to_owned)
3919        .collect()
3920}
3921
3922fn annotate_rust_label(label: &str, semantic_types: &[String]) -> String {
3923    let Some(arguments_start) = outer_call_arguments_start(label) else {
3924        return label.to_owned();
3925    };
3926    let Some(arguments_end) = matching_parenthesis(label, arguments_start) else {
3927        return label.to_owned();
3928    };
3929    let arguments = split_rust_arguments(&label[arguments_start + 1..arguments_end]);
3930    if arguments.is_empty() || semantic_types.is_empty() {
3931        return label.to_owned();
3932    }
3933    let types = if semantic_types.len() >= arguments.len() {
3934        &semantic_types[semantic_types.len() - arguments.len()..]
3935    } else {
3936        semantic_types
3937    };
3938    let leading_untyped = arguments.len().saturating_sub(types.len());
3939    let annotated = arguments
3940        .iter()
3941        .enumerate()
3942        .map(|(index, argument)| {
3943            if index < leading_untyped {
3944                argument.to_string()
3945            } else {
3946                format!("{}: {}", argument, types[index - leading_untyped])
3947            }
3948        })
3949        .collect::<Vec<_>>()
3950        .join(", ");
3951    format!(
3952        "{}({annotated}){}",
3953        &label[..arguments_start],
3954        &label[arguments_end + 1..]
3955    )
3956}
3957
3958fn matching_parenthesis(label: &str, start: usize) -> Option<usize> {
3959    let mut depth = 0usize;
3960    let mut in_string = false;
3961    let mut escaped = false;
3962    for (offset, character) in label[start..].char_indices() {
3963        if in_string {
3964            if escaped {
3965                escaped = false;
3966            } else if character == '\\' {
3967                escaped = true;
3968            } else if character == '"' {
3969                in_string = false;
3970            }
3971            continue;
3972        }
3973        match character {
3974            '"' => in_string = true,
3975            '(' => depth += 1,
3976            ')' => {
3977                depth = depth.checked_sub(1)?;
3978                if depth == 0 {
3979                    return Some(start + offset);
3980                }
3981            }
3982            _ => {}
3983        }
3984    }
3985    None
3986}
3987
3988fn split_rust_arguments(arguments: &str) -> Vec<&str> {
3989    if arguments.trim().is_empty() {
3990        return Vec::new();
3991    }
3992    let mut result = Vec::new();
3993    let mut start = 0usize;
3994    let mut delimiters = Vec::new();
3995    let mut in_string = false;
3996    let mut escaped = false;
3997    for (index, character) in arguments.char_indices() {
3998        if in_string {
3999            if escaped {
4000                escaped = false;
4001            } else if character == '\\' {
4002                escaped = true;
4003            } else if character == '"' {
4004                in_string = false;
4005            }
4006            continue;
4007        }
4008        match character {
4009            '"' => in_string = true,
4010            '(' | '[' | '{' | '<' => delimiters.push(character),
4011            ')' | ']' | '}' | '>' => {
4012                delimiters.pop();
4013            }
4014            ',' if delimiters.is_empty() => {
4015                result.push(arguments[start..index].trim());
4016                start = index + character.len_utf8();
4017            }
4018            _ => {}
4019        }
4020    }
4021    result.push(arguments[start..].trim());
4022    result
4023}
4024
4025fn outer_call_arguments_start(label: &str) -> Option<usize> {
4026    let mut depth = 0usize;
4027    for (index, character) in label.char_indices().rev() {
4028        match character {
4029            ')' => depth += 1,
4030            '(' => {
4031                depth = depth.checked_sub(1)?;
4032                if depth == 0 {
4033                    return Some(index);
4034                }
4035            }
4036            _ => {}
4037        }
4038    }
4039    None
4040}
4041
4042fn rustc_source_span(span: rustc_public::ty::Span) -> SourceSpan {
4043    let lines = span.get_lines();
4044    let file = PathBuf::from(span.get_filename());
4045    let file = if file.is_absolute() {
4046        file
4047    } else {
4048        env::current_dir().map_or(file.clone(), |directory| directory.join(file))
4049    };
4050    SourceSpan {
4051        file,
4052        start_line: lines.start_line,
4053        start_column: lines.start_col.saturating_sub(1),
4054        start_byte: None,
4055        end_line: lines.end_line.max(lines.start_line),
4056        end_column: lines.end_col.saturating_sub(1),
4057        end_byte: None,
4058    }
4059}
4060
4061fn source_files_match(left: &Path, right: &Path) -> bool {
4062    left == right
4063        || left
4064            .canonicalize()
4065            .ok()
4066            .zip(right.canonicalize().ok())
4067            .is_some_and(|(left, right)| left == right)
4068}
4069
4070fn span_contains(outer: &SourceSpan, inner: &SourceSpan) -> bool {
4071    position_le(
4072        outer.start_line,
4073        outer.start_column,
4074        inner.start_line,
4075        inner.start_column,
4076    ) && position_le(
4077        inner.end_line,
4078        inner.end_column,
4079        outer.end_line,
4080        outer.end_column,
4081    )
4082}
4083
4084fn spans_overlap(left: &SourceSpan, right: &SourceSpan) -> bool {
4085    position_le(
4086        left.start_line,
4087        left.start_column,
4088        right.end_line,
4089        right.end_column,
4090    ) && position_le(
4091        right.start_line,
4092        right.start_column,
4093        left.end_line,
4094        left.end_column,
4095    )
4096}
4097
4098fn position_le(
4099    left_line: usize,
4100    left_column: usize,
4101    right_line: usize,
4102    right_column: usize,
4103) -> bool {
4104    (left_line, left_column) <= (right_line, right_column)
4105}
4106
4107fn span_size(span: &SourceSpan) -> usize {
4108    (span.end_line.saturating_sub(span.start_line) * 10_000)
4109        + span.end_column.saturating_sub(span.start_column)
4110}
4111
4112fn span_distance(left: &SourceSpan, right: &SourceSpan) -> usize {
4113    left.start_line.abs_diff(right.start_line) * 10_000
4114        + left.start_column.abs_diff(right.start_column)
4115}
4116
4117impl LanguageBackend for RustBackend {
4118    fn language(&self) -> LanguageId {
4119        LanguageId::new("rust")
4120    }
4121
4122    fn extensions(&self) -> &'static [&'static str] {
4123        &["rs"]
4124    }
4125
4126    fn analyze_file(
4127        &self,
4128        context: &FileContext<'_>,
4129        source: &str,
4130    ) -> FrontendResult<FileAnalysis> {
4131        let syntax = syn::parse_file(source)?;
4132        let mut analysis = FileAnalysis::default();
4133        analysis.source_files.insert(context.path.to_path_buf());
4134        analyze_items(context.path, context.module, &syntax.items, &mut analysis);
4135        Ok(analysis)
4136    }
4137
4138    fn analyze_project(&self, context: &ProjectContext<'_>) -> FrontendResult<FileAnalysis> {
4139        let wrapper = context.driver_executable.ok_or_else(|| {
4140            std::io::Error::other("Rust project analysis requires a driver executable")
4141        })?;
4142        let session = match context.cache {
4143            Some(cache) => RustProjectSession::create_cached(
4144                cache.project_root,
4145                cache.endpoint,
4146                cache.verbose,
4147            )?,
4148            None => RustProjectSession::create()?,
4149        };
4150        analyze_semantic_project(context.root, wrapper, context.entries, &session)
4151    }
4152}
4153
4154fn analyze_items(file: &Path, module: &[String], items: &[Item], analysis: &mut FileAnalysis) {
4155    for item in items {
4156        match item {
4157            Item::Fn(function) => {
4158                let function_info = function_from_item(file, module, None, function);
4159                let owner = function_info.id.clone();
4160                add_signature_facts(file, &function_info, &function.sig, analysis);
4161                analysis.functions.push(function_info);
4162                add_closure_functions(file, module, &owner, &function.block, analysis);
4163            }
4164            Item::Impl(item_impl) => analyze_impl(file, module, item_impl, analysis),
4165            Item::Trait(item_trait) => analyze_trait(file, module, item_trait, analysis),
4166            Item::Mod(item_mod) => {
4167                if let Some((_, nested)) = &item_mod.content {
4168                    let mut nested_module = module.to_vec();
4169                    nested_module.push(item_mod.ident.to_string());
4170                    analyze_items(file, &nested_module, nested, analysis);
4171                }
4172            }
4173            _ => {}
4174        }
4175    }
4176}
4177
4178fn function_from_item(
4179    file: &Path,
4180    module: &[String],
4181    container: Option<String>,
4182    function: &ItemFn,
4183) -> FunctionInfo {
4184    function_from_parts(
4185        file,
4186        module,
4187        container,
4188        &function.sig,
4189        &function.block,
4190        is_public(&function.vis),
4191        function.span(),
4192    )
4193}
4194
4195fn analyze_impl(file: &Path, module: &[String], item_impl: &ItemImpl, analysis: &mut FileAnalysis) {
4196    let self_ty = compact_tokens(&item_impl.self_ty);
4197    let trait_name = item_impl
4198        .trait_
4199        .as_ref()
4200        .map(|(_, path, _)| compact_tokens(path));
4201    let container = trait_name
4202        .as_ref()
4203        .map(|trait_name| format!("{self_ty} as {trait_name}"))
4204        .unwrap_or_else(|| self_ty.clone());
4205
4206    for item in &item_impl.items {
4207        let ImplItem::Fn(method) = item else {
4208            continue;
4209        };
4210        let function = function_from_parts(
4211            file,
4212            module,
4213            Some(container.clone()),
4214            &method.sig,
4215            &method.block,
4216            is_public(&method.vis) || trait_name.is_some(),
4217            method.span(),
4218        );
4219        let owner = function.id.clone();
4220
4221        add_signature_facts(file, &function, &method.sig, analysis);
4222        if let Some(trait_name) = &trait_name {
4223            analysis.facts.push(LanguageFact {
4224                subject: function.id.clone(),
4225                namespace: LanguageId::new("rust"),
4226                kind: "impl-trait".to_owned(),
4227                key: trait_name.clone(),
4228                value: self_ty.clone(),
4229                span: function.span.clone(),
4230            });
4231        }
4232        analysis.functions.push(function);
4233        add_closure_functions(file, module, &owner, &method.block, analysis);
4234    }
4235}
4236
4237fn analyze_trait(
4238    file: &Path,
4239    module: &[String],
4240    item_trait: &ItemTrait,
4241    analysis: &mut FileAnalysis,
4242) {
4243    let trait_name = item_trait.ident.to_string();
4244    for item in &item_trait.items {
4245        let TraitItem::Fn(method) = item else {
4246            continue;
4247        };
4248        let Some(block) = &method.default else {
4249            continue;
4250        };
4251        let function = function_from_parts(
4252            file,
4253            module,
4254            Some(trait_name.clone()),
4255            &method.sig,
4256            block,
4257            is_public(&item_trait.vis),
4258            method.span(),
4259        );
4260        let owner = function.id.clone();
4261        add_signature_facts(file, &function, &method.sig, analysis);
4262        analysis.functions.push(function);
4263        add_closure_functions(file, module, &owner, block, analysis);
4264    }
4265}
4266
4267fn add_closure_functions(
4268    file: &Path,
4269    module: &[String],
4270    owner: &SymbolId,
4271    block: &syn::Block,
4272    analysis: &mut FileAnalysis,
4273) {
4274    let closure_sources = closure_sources(block);
4275    let mut extractor = ClosureExtractor {
4276        file,
4277        module,
4278        owner,
4279        closure_sources: &closure_sources,
4280        functions: Vec::new(),
4281    };
4282    extractor.visit_block(block);
4283    analysis.functions.extend(extractor.functions);
4284}
4285
4286struct ClosureExtractor<'a> {
4287    file: &'a Path,
4288    module: &'a [String],
4289    owner: &'a SymbolId,
4290    closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
4291    functions: Vec<FunctionInfo>,
4292}
4293
4294impl<'ast> Visit<'ast> for ClosureExtractor<'_> {
4295    fn visit_expr_closure(&mut self, node: &'ast ExprClosure) {
4296        let source = self
4297            .closure_sources
4298            .get(&closure_span(node.span()))
4299            .cloned()
4300            .unwrap_or_else(|| ClosureSource::anonymous(self.functions.len()));
4301        let closure_name = format!("{{lambda:{}}}", source.identity);
4302        let lambda = source.lambda_name();
4303        let id = SymbolId {
4304            language: LanguageId::new("rust"),
4305            module: self.module.to_vec(),
4306            container: Some(self.owner.qualified_parts().join("::")),
4307            name: closure_name.clone(),
4308        };
4309        let mut calls = CallCollector {
4310            file: self.file,
4311            closure_sources: self.closure_sources,
4312            calls: Vec::new(),
4313        };
4314        calls.visit_expr(&node.body);
4315        let parameters = node.inputs.iter().map(pattern_name).collect::<Vec<_>>();
4316        let typed_parameters = node
4317            .inputs
4318            .iter()
4319            .map(|parameter| match parameter {
4320                Pat::Type(typed) => format!(
4321                    "{}: {}",
4322                    pattern_name(&typed.pat),
4323                    compact_tokens(&typed.ty)
4324                ),
4325                parameter => pattern_name(parameter),
4326            })
4327            .collect::<Vec<_>>();
4328        self.functions.push(FunctionInfo {
4329            id,
4330            label: CallLabel::with_types(
4331                format!("{lambda}({})", parameters.join(", ")),
4332                format!("{lambda}({})", typed_parameters.join(", ")),
4333            ),
4334            public: false,
4335            calls: calls.calls,
4336            span: source_span(self.file, node.span()),
4337        });
4338        visit::visit_expr_closure(self, node);
4339    }
4340
4341    fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
4342}
4343
4344fn function_from_parts(
4345    file: &Path,
4346    module: &[String],
4347    container: Option<String>,
4348    signature: &Signature,
4349    block: &syn::Block,
4350    public: bool,
4351    span: Span,
4352) -> FunctionInfo {
4353    let id = SymbolId {
4354        language: LanguageId::new("rust"),
4355        module: module.to_vec(),
4356        container,
4357        name: signature.ident.to_string(),
4358    };
4359    let closure_sources = closure_sources(block);
4360    let mut collector = CallCollector {
4361        file,
4362        closure_sources: &closure_sources,
4363        calls: Vec::new(),
4364    };
4365    collector.visit_block(block);
4366    rewrite_symbolic_generic_calls(module, signature, &mut collector.calls);
4367
4368    FunctionInfo {
4369        label: function_label(&id, signature),
4370        id,
4371        public,
4372        calls: collector.calls,
4373        span: source_span(file, span),
4374    }
4375}
4376
4377fn add_signature_facts(
4378    file: &Path,
4379    function: &FunctionInfo,
4380    signature: &Signature,
4381    analysis: &mut FileAnalysis,
4382) {
4383    for parameter in &signature.generics.params {
4384        let (key, value) = match parameter {
4385            syn::GenericParam::Lifetime(parameter) => (
4386                parameter.lifetime.to_string(),
4387                parameter
4388                    .bounds
4389                    .iter()
4390                    .map(ToString::to_string)
4391                    .collect::<Vec<_>>()
4392                    .join(" + "),
4393            ),
4394            syn::GenericParam::Type(parameter) => (
4395                parameter.ident.to_string(),
4396                parameter
4397                    .bounds
4398                    .iter()
4399                    .map(compact_tokens)
4400                    .collect::<Vec<_>>()
4401                    .join(" + "),
4402            ),
4403            syn::GenericParam::Const(parameter) => {
4404                (parameter.ident.to_string(), compact_tokens(&parameter.ty))
4405            }
4406        };
4407        analysis.facts.push(LanguageFact {
4408            subject: function.id.clone(),
4409            namespace: LanguageId::new("rust"),
4410            kind: "generic-parameter".to_owned(),
4411            key,
4412            value,
4413            span: source_span(file, parameter.span()),
4414        });
4415    }
4416    for input in &signature.inputs {
4417        match input {
4418            FnArg::Receiver(receiver) => {
4419                let mode = match (&receiver.reference, receiver.mutability.is_some()) {
4420                    (Some(_), true) => "&mut self",
4421                    (Some(_), false) => "&self",
4422                    (None, true) => "mut self",
4423                    (None, false) => "self",
4424                };
4425                analysis.facts.push(LanguageFact {
4426                    subject: function.id.clone(),
4427                    namespace: LanguageId::new("rust"),
4428                    kind: "receiver".to_owned(),
4429                    key: "self".to_owned(),
4430                    value: mode.to_owned(),
4431                    span: source_span(file, receiver.span()),
4432                });
4433            }
4434            FnArg::Typed(argument) => {
4435                analysis.facts.push(LanguageFact {
4436                    subject: function.id.clone(),
4437                    namespace: LanguageId::new("rust"),
4438                    kind: "parameter".to_owned(),
4439                    key: compact_tokens(&argument.pat),
4440                    value: compact_tokens(&argument.ty),
4441                    span: source_span(file, argument.span()),
4442                });
4443            }
4444        }
4445    }
4446
4447    if signature.asyncness.is_some() {
4448        push_modifier_fact(function, "async", file, signature.span(), analysis);
4449    }
4450    if signature.unsafety.is_some() {
4451        push_modifier_fact(function, "unsafe", file, signature.span(), analysis);
4452    }
4453    if signature.constness.is_some() {
4454        push_modifier_fact(function, "const", file, signature.span(), analysis);
4455    }
4456    if let ReturnType::Type(_, ty) = &signature.output {
4457        analysis.facts.push(LanguageFact {
4458            subject: function.id.clone(),
4459            namespace: LanguageId::new("rust"),
4460            kind: "return-type".to_owned(),
4461            key: "return".to_owned(),
4462            value: compact_tokens(ty),
4463            span: source_span(file, ty.span()),
4464        });
4465    }
4466}
4467
4468fn push_modifier_fact(
4469    function: &FunctionInfo,
4470    modifier: &str,
4471    file: &Path,
4472    span: Span,
4473    analysis: &mut FileAnalysis,
4474) {
4475    analysis.facts.push(LanguageFact {
4476        subject: function.id.clone(),
4477        namespace: LanguageId::new("rust"),
4478        kind: "modifier".to_owned(),
4479        key: modifier.to_owned(),
4480        value: "true".to_owned(),
4481        span: source_span(file, span),
4482    });
4483}
4484
4485fn parameter_names(signature: &Signature) -> String {
4486    signature
4487        .inputs
4488        .iter()
4489        .map(|input| match input {
4490            FnArg::Receiver(receiver) => match (&receiver.reference, receiver.mutability.is_some())
4491            {
4492                (Some(_), true) => "&mut self".to_owned(),
4493                (Some(_), false) => "&self".to_owned(),
4494                (None, true) => "mut self".to_owned(),
4495                (None, false) => "self".to_owned(),
4496            },
4497            FnArg::Typed(argument) => pattern_name(&argument.pat),
4498        })
4499        .collect::<Vec<_>>()
4500        .join(", ")
4501}
4502
4503fn function_label(id: &SymbolId, signature: &Signature) -> CallLabel {
4504    let generic_names = generic_parameter_labels(signature);
4505    let generics = if generic_names.is_empty() {
4506        String::new()
4507    } else {
4508        format!("<{}>", generic_names.join(", "))
4509    };
4510    let default = format!(
4511        "{}{generics}({})",
4512        id.short_name(),
4513        parameter_names(signature)
4514    );
4515    let typed = format!(
4516        "{}{generics}({})",
4517        id.short_name(),
4518        typed_parameters(signature)
4519    );
4520    CallLabel::with_types(default, typed)
4521}
4522
4523fn generic_parameter_labels(signature: &Signature) -> Vec<String> {
4524    let mut where_bounds = HashMap::<String, Vec<String>>::new();
4525    if let Some(where_clause) = &signature.generics.where_clause {
4526        for predicate in &where_clause.predicates {
4527            let syn::WherePredicate::Type(predicate) = predicate else {
4528                continue;
4529            };
4530            let key = compact_tokens(&predicate.bounded_ty);
4531            where_bounds
4532                .entry(key)
4533                .or_default()
4534                .extend(predicate.bounds.iter().map(compact_tokens));
4535        }
4536    }
4537
4538    signature
4539        .generics
4540        .params
4541        .iter()
4542        .map(|parameter| match parameter {
4543            syn::GenericParam::Lifetime(parameter) => {
4544                let mut bounds = parameter
4545                    .bounds
4546                    .iter()
4547                    .map(ToString::to_string)
4548                    .collect::<Vec<_>>();
4549                bounds.extend(
4550                    where_bounds
4551                        .get(&parameter.lifetime.to_string())
4552                        .cloned()
4553                        .unwrap_or_default(),
4554                );
4555                format_generic_parameter(parameter.lifetime.to_string(), bounds)
4556            }
4557            syn::GenericParam::Type(parameter) => {
4558                let name = parameter.ident.to_string();
4559                let mut bounds = parameter
4560                    .bounds
4561                    .iter()
4562                    .map(compact_tokens)
4563                    .collect::<Vec<_>>();
4564                bounds.extend(where_bounds.get(&name).cloned().unwrap_or_default());
4565                format_generic_parameter(name, bounds)
4566            }
4567            syn::GenericParam::Const(parameter) => {
4568                format!(
4569                    "const {}: {}",
4570                    parameter.ident,
4571                    compact_tokens(&parameter.ty)
4572                )
4573            }
4574        })
4575        .collect()
4576}
4577
4578fn format_generic_parameter(name: String, mut bounds: Vec<String>) -> String {
4579    bounds.retain(|bound| !bound.is_empty());
4580    bounds.sort();
4581    bounds.dedup();
4582    if bounds.is_empty() {
4583        name
4584    } else {
4585        format!("{name}: {}", bounds.join(" + "))
4586    }
4587}
4588
4589fn rewrite_symbolic_generic_calls(
4590    module: &[String],
4591    signature: &Signature,
4592    calls: &mut [CallSite],
4593) {
4594    let generic_bounds = generic_type_bounds(signature);
4595    if generic_bounds.is_empty() {
4596        return;
4597    }
4598    let receivers = signature
4599        .inputs
4600        .iter()
4601        .filter_map(|input| {
4602            let FnArg::Typed(argument) = input else {
4603                return None;
4604            };
4605            let parameter = simple_pattern_binding(&argument.pat)?;
4606            let generic = simple_generic_type(&argument.ty)?;
4607            generic_bounds
4608                .contains_key(&generic)
4609                .then_some((parameter, generic))
4610        })
4611        .collect::<HashMap<_, _>>();
4612
4613    for call in calls {
4614        let CallSyntax::Method { receiver, method } = &call.syntax else {
4615            continue;
4616        };
4617        let Some(generic) = receivers.get(receiver) else {
4618            continue;
4619        };
4620        let bound = generic_bounds
4621            .get(generic)
4622            .and_then(|bounds| (bounds.len() == 1).then(|| bounds[0].clone()));
4623        call.target = CallTarget::Direct(SymbolId {
4624            language: LanguageId::new("rust"),
4625            module: module.to_vec(),
4626            container: Some(
4627                bound.map_or_else(|| generic.clone(), |bound| format!("{generic} as {bound}")),
4628            ),
4629            name: method.clone(),
4630        });
4631        call.label = replace_label_callee(&call.label, &format!("{generic}::{method}"));
4632    }
4633}
4634
4635fn generic_type_bounds(signature: &Signature) -> HashMap<String, Vec<String>> {
4636    let mut result = HashMap::<String, Vec<String>>::new();
4637    for parameter in &signature.generics.params {
4638        let syn::GenericParam::Type(parameter) = parameter else {
4639            continue;
4640        };
4641        result.insert(
4642            parameter.ident.to_string(),
4643            parameter.bounds.iter().map(compact_tokens).collect(),
4644        );
4645    }
4646    if let Some(where_clause) = &signature.generics.where_clause {
4647        for predicate in &where_clause.predicates {
4648            let syn::WherePredicate::Type(predicate) = predicate else {
4649                continue;
4650            };
4651            let name = compact_tokens(&predicate.bounded_ty);
4652            if let Some(bounds) = result.get_mut(&name) {
4653                bounds.extend(predicate.bounds.iter().map(compact_tokens));
4654            }
4655        }
4656    }
4657    for bounds in result.values_mut() {
4658        bounds.sort();
4659        bounds.dedup();
4660    }
4661    result
4662}
4663
4664fn simple_generic_type(ty: &syn::Type) -> Option<String> {
4665    match ty {
4666        syn::Type::Path(path) if path.qself.is_none() && path.path.segments.len() == 1 => {
4667            Some(path.path.segments[0].ident.to_string())
4668        }
4669        syn::Type::Reference(reference) => simple_generic_type(&reference.elem),
4670        syn::Type::Group(group) => simple_generic_type(&group.elem),
4671        syn::Type::Paren(paren) => simple_generic_type(&paren.elem),
4672        _ => None,
4673    }
4674}
4675
4676fn typed_parameters(signature: &Signature) -> String {
4677    signature
4678        .inputs
4679        .iter()
4680        .map(|input| match input {
4681            FnArg::Receiver(_) => pattern_name_from_argument(input),
4682            FnArg::Typed(argument) => {
4683                format!(
4684                    "{}: {}",
4685                    pattern_name(&argument.pat),
4686                    compact_tokens(&argument.ty)
4687                )
4688            }
4689        })
4690        .collect::<Vec<_>>()
4691        .join(", ")
4692}
4693
4694fn pattern_name_from_argument(argument: &FnArg) -> String {
4695    match argument {
4696        FnArg::Receiver(receiver) => match (&receiver.reference, receiver.mutability.is_some()) {
4697            (Some(_), true) => "&mut self".to_owned(),
4698            (Some(_), false) => "&self".to_owned(),
4699            (None, true) => "mut self".to_owned(),
4700            (None, false) => "self".to_owned(),
4701        },
4702        FnArg::Typed(argument) => pattern_name(&argument.pat),
4703    }
4704}
4705
4706fn pattern_name(pattern: &Pat) -> String {
4707    match pattern {
4708        Pat::Ident(ident) => ident.ident.to_string(),
4709        Pat::Reference(reference) => pattern_name(&reference.pat),
4710        Pat::Wild(_) => "_".to_owned(),
4711        _ => compact_tokens(pattern),
4712    }
4713}
4714
4715fn is_public(visibility: &Visibility) -> bool {
4716    matches!(visibility, Visibility::Public(_))
4717}
4718
4719fn compact_tokens(tokens: &impl ToTokens) -> String {
4720    tokens.to_token_stream().to_string().replace(' ', "")
4721}
4722
4723fn source_span(file: &Path, span: Span) -> SourceSpan {
4724    let start = span.start();
4725    let end = span.end();
4726    SourceSpan {
4727        file: file.to_path_buf(),
4728        start_line: start.line,
4729        start_column: start.column,
4730        start_byte: None,
4731        end_line: end.line.max(start.line),
4732        end_column: end.column,
4733        end_byte: None,
4734    }
4735}
4736
4737struct CallCollector<'a> {
4738    file: &'a Path,
4739    closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
4740    calls: Vec<CallSite>,
4741}
4742
4743impl<'ast> Visit<'ast> for CallCollector<'_> {
4744    fn visit_expr_call(&mut self, node: &'ast ExprCall) {
4745        // MIR evaluates nested calls before their enclosing expression. Keep
4746        // the same order so an aggregate constructor such as `Some(f())`
4747        // cannot claim `f`'s semantic call span.
4748        visit::visit_expr_call(self, node);
4749        if let Some(parts) = callable_path(&node.func) {
4750            let syntax = CallSyntax::Path(parts);
4751            let span = source_span(self.file, node.span());
4752            self.calls.push(CallSite {
4753                id: CallSiteId::source(&syntax, &span),
4754                syntax,
4755                target: CallTarget::Unresolved,
4756                label: CallLabel::new(call_expression_label(node, self.closure_sources)),
4757                span,
4758            });
4759        }
4760    }
4761
4762    fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
4763        visit::visit_expr_method_call(self, node);
4764        let method = node.method.to_string();
4765        let syntax = if is_self_expr(&node.receiver) {
4766            CallSyntax::SelfMethod(method)
4767        } else {
4768            CallSyntax::Method {
4769                receiver: receiver_name(&node.receiver),
4770                method,
4771            }
4772        };
4773        let span = source_span(self.file, node.span());
4774        self.calls.push(CallSite {
4775            id: CallSiteId::source(&syntax, &span),
4776            syntax,
4777            target: CallTarget::Unresolved,
4778            label: CallLabel::new(method_call_label(node, self.closure_sources)),
4779            span,
4780        });
4781    }
4782
4783    // Nested closures and local functions own their calls; do not attribute them
4784    // to the enclosing function.
4785    fn visit_expr_closure(&mut self, _node: &'ast ExprClosure) {}
4786
4787    fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
4788}
4789
4790fn call_expression_label(
4791    node: &ExprCall,
4792    closure_sources: &HashMap<ClosureSpan, ClosureSource>,
4793) -> String {
4794    let function = compact_tokens(&node.func).replace("::<", "<");
4795    let (closure_types, arguments) = call_arguments(&node.args, closure_sources);
4796    let function = append_generic_arguments(&function, &closure_types);
4797    format!("{function}({})", arguments.join(", "))
4798}
4799
4800fn method_call_label(
4801    node: &ExprMethodCall,
4802    closure_sources: &HashMap<ClosureSpan, ClosureSource>,
4803) -> String {
4804    let receiver = simple_receiver(&node.receiver)
4805        .then(|| compact_expression(&node.receiver, closure_sources));
4806    let generics = node
4807        .turbofish
4808        .as_ref()
4809        .map(compact_tokens)
4810        .unwrap_or_default()
4811        .replace("::<", "<");
4812    let (closure_types, arguments) = call_arguments(&node.args, closure_sources);
4813    let method = format!("{}{generics}", node.method);
4814    let callee = receiver.map_or(method.clone(), |receiver| format!("{receiver}.{method}"));
4815    let callee = append_generic_arguments(&callee, &closure_types);
4816    format!("{callee}({})", arguments.join(", "))
4817}
4818
4819type ClosureSpan = (usize, usize, usize, usize);
4820
4821#[derive(Clone, Debug)]
4822struct ClosureSource {
4823    ordinal: usize,
4824    binding: Option<String>,
4825    identity: String,
4826}
4827
4828impl ClosureSource {
4829    fn anonymous(ordinal: usize) -> Self {
4830        Self {
4831            ordinal,
4832            binding: None,
4833            identity: format!("fallback-{ordinal}"),
4834        }
4835    }
4836
4837    fn lambda_name(&self) -> String {
4838        self.binding.as_ref().map_or_else(
4839            || format!("λ#{}", self.ordinal + 1),
4840            |name| format!("λ{name}"),
4841        )
4842    }
4843}
4844
4845fn closure_span(span: Span) -> ClosureSpan {
4846    let start = span.start();
4847    let end = span.end();
4848    (start.line, start.column, end.line, end.column)
4849}
4850
4851fn closure_sources(block: &syn::Block) -> HashMap<ClosureSpan, ClosureSource> {
4852    let mut collector = ClosureOrdinalCollector::default();
4853    collector.visit_block(block);
4854    collector.sources
4855}
4856
4857#[derive(Default)]
4858struct ClosureOrdinalCollector {
4859    sources: HashMap<ClosureSpan, ClosureSource>,
4860    bindings: HashMap<ClosureSpan, String>,
4861    identity_counts: HashMap<String, usize>,
4862}
4863
4864impl<'ast> Visit<'ast> for ClosureOrdinalCollector {
4865    fn visit_local(&mut self, node: &'ast syn::Local) {
4866        if let Some(initializer) = &node.init
4867            && let Some(closure) = direct_closure_expression(&initializer.expr)
4868            && let Some(binding) = simple_pattern_binding(&node.pat)
4869        {
4870            self.bindings.insert(closure_span(closure.span()), binding);
4871        }
4872        visit::visit_local(self, node);
4873    }
4874
4875    fn visit_expr_closure(&mut self, node: &'ast ExprClosure) {
4876        let span = closure_span(node.span());
4877        let ordinal = self.sources.len();
4878        let base_identity = closure_structural_identity(node);
4879        let occurrence = self
4880            .identity_counts
4881            .entry(base_identity.clone())
4882            .or_default();
4883        let identity = if *occurrence == 0 {
4884            base_identity
4885        } else {
4886            format!("{base_identity}-{}", *occurrence + 1)
4887        };
4888        *occurrence += 1;
4889        self.sources.insert(
4890            span,
4891            ClosureSource {
4892                ordinal,
4893                binding: self.bindings.get(&span).cloned(),
4894                identity,
4895            },
4896        );
4897        visit::visit_expr_closure(self, node);
4898    }
4899
4900    fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
4901}
4902
4903fn closure_structural_identity(node: &ExprClosure) -> String {
4904    let signature = format!(
4905        "{}:{}:{}:{}",
4906        node.asyncness.is_some(),
4907        node.movability.is_some(),
4908        node.capture.is_some(),
4909        node.inputs
4910            .iter()
4911            .map(compact_tokens)
4912            .collect::<Vec<_>>()
4913            .join(",")
4914    );
4915    let mut hasher = Sha256::new();
4916    hasher.update(signature.as_bytes());
4917    format!("{:x}", hasher.finalize())[..12].to_owned()
4918}
4919
4920struct ClosureLabelRedactor<'a> {
4921    closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
4922    used: Vec<(String, String)>,
4923}
4924
4925impl VisitMut for ClosureLabelRedactor<'_> {
4926    fn visit_expr_mut(&mut self, node: &mut Expr) {
4927        let Expr::Closure(closure) = node else {
4928            visit_mut::visit_expr_mut(self, node);
4929            return;
4930        };
4931        let Some(source) = self
4932            .closure_sources
4933            .get(&closure_span(closure.span()))
4934            .cloned()
4935        else {
4936            return;
4937        };
4938        let marker = format!("__diffkit_closure_placeholder_{}", source.ordinal);
4939        self.used.push((marker.clone(), source.lambda_name()));
4940        let ident = syn::Ident::new(&marker, closure.span());
4941        *node = Expr::Path(syn::ExprPath {
4942            attrs: Vec::new(),
4943            qself: None,
4944            path: syn::Path::from(ident),
4945        });
4946    }
4947}
4948
4949fn compact_expression(
4950    expression: &Expr,
4951    closure_sources: &HashMap<ClosureSpan, ClosureSource>,
4952) -> String {
4953    let mut expression = expression.clone();
4954    let mut redactor = ClosureLabelRedactor {
4955        closure_sources,
4956        used: Vec::new(),
4957    };
4958    redactor.visit_expr_mut(&mut expression);
4959    let mut rendered = compact_tokens(&expression);
4960    for (marker, lambda) in redactor.used {
4961        rendered = rendered.replace(&marker, &lambda);
4962    }
4963    rendered
4964}
4965
4966fn call_argument_label(
4967    expression: &Expr,
4968    closure_sources: &HashMap<ClosureSpan, ClosureSource>,
4969) -> String {
4970    let rendered = compact_expression(expression, closure_sources);
4971    if rendered.chars().count() <= 96 {
4972        rendered
4973    } else {
4974        "…".to_owned()
4975    }
4976}
4977
4978fn call_arguments(
4979    arguments: &syn::punctuated::Punctuated<Expr, syn::Token![,]>,
4980    closure_sources: &HashMap<ClosureSpan, ClosureSource>,
4981) -> (Vec<String>, Vec<String>) {
4982    let mut closure_types = Vec::new();
4983    let mut values = Vec::new();
4984    for argument in arguments {
4985        if let Some(source) = closure_source_for_expression(argument, closure_sources) {
4986            closure_types.push(source.lambda_name());
4987        } else {
4988            values.push(call_argument_label(argument, closure_sources));
4989        }
4990    }
4991    (closure_types, values)
4992}
4993
4994fn closure_source_for_expression<'a>(
4995    expression: &Expr,
4996    closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
4997) -> Option<&'a ClosureSource> {
4998    if let Some(closure) = direct_closure_expression(expression) {
4999        return closure_sources.get(&closure_span(closure.span()));
5000    }
5001    let Expr::Path(path) = expression else {
5002        return None;
5003    };
5004    let binding = path.path.get_ident()?.to_string();
5005    closure_sources
5006        .values()
5007        .find(|source| source.binding.as_deref() == Some(binding.as_str()))
5008}
5009
5010fn direct_closure_expression(expression: &Expr) -> Option<&ExprClosure> {
5011    match expression {
5012        Expr::Closure(closure) => Some(closure),
5013        Expr::Group(group) => direct_closure_expression(&group.expr),
5014        Expr::Paren(paren) => direct_closure_expression(&paren.expr),
5015        _ => None,
5016    }
5017}
5018
5019fn simple_pattern_binding(pattern: &Pat) -> Option<String> {
5020    match pattern {
5021        Pat::Ident(identifier) => Some(identifier.ident.to_string()),
5022        Pat::Type(typed) => simple_pattern_binding(&typed.pat),
5023        _ => None,
5024    }
5025}
5026
5027fn append_generic_arguments(callee: &str, additions: &[String]) -> String {
5028    if additions.is_empty() {
5029        return callee.to_owned();
5030    }
5031    let additions = additions.join(", ");
5032    let Some(start) = trailing_generic_arguments_start(callee) else {
5033        return format!("{callee}<{additions}>");
5034    };
5035    let existing = &callee[start + 1..callee.len() - 1];
5036    let separator = if existing.is_empty() { "" } else { ", " };
5037    format!("{}<{existing}{separator}{additions}>", &callee[..start])
5038}
5039
5040fn trailing_generic_arguments_start(value: &str) -> Option<usize> {
5041    if !value.ends_with('>') {
5042        return None;
5043    }
5044    let mut depth = 0usize;
5045    for (index, character) in value.char_indices().rev() {
5046        match character {
5047            '>' => depth += 1,
5048            '<' => {
5049                depth = depth.checked_sub(1)?;
5050                if depth == 0 {
5051                    return Some(index);
5052                }
5053            }
5054            _ => {}
5055        }
5056    }
5057    None
5058}
5059
5060fn simple_receiver(expression: &Expr) -> bool {
5061    match expression {
5062        Expr::Path(_) => true,
5063        Expr::Field(field) => simple_receiver(&field.base),
5064        Expr::Index(index) => simple_receiver(&index.expr),
5065        Expr::Paren(paren) => simple_receiver(&paren.expr),
5066        Expr::Group(group) => simple_receiver(&group.expr),
5067        Expr::Reference(reference) => simple_receiver(&reference.expr),
5068        Expr::Unary(unary) => simple_receiver(&unary.expr),
5069        _ => false,
5070    }
5071}
5072
5073fn callable_path(expression: &Expr) -> Option<Vec<String>> {
5074    match expression {
5075        Expr::Path(path) => Some(
5076            path.path
5077                .segments
5078                .iter()
5079                .map(|segment| segment.ident.to_string())
5080                .collect(),
5081        ),
5082        Expr::Group(group) => callable_path(&group.expr),
5083        Expr::Paren(paren) => callable_path(&paren.expr),
5084        _ => None,
5085    }
5086}
5087
5088fn is_self_expr(expression: &Expr) -> bool {
5089    matches!(expression, Expr::Path(path) if path.path.is_ident("self"))
5090}
5091
5092fn receiver_name(expression: &Expr) -> String {
5093    match expression {
5094        Expr::Path(path) => path
5095            .path
5096            .segments
5097            .iter()
5098            .map(|segment| segment.ident.to_string())
5099            .collect::<Vec<_>>()
5100            .join("::"),
5101        _ => "<expr>".to_owned(),
5102    }
5103}
5104
5105#[cfg(test)]
5106mod tests {
5107    use std::fs;
5108    use std::path::Path;
5109
5110    use super::*;
5111
5112    #[test]
5113    fn extracts_functions_methods_calls_and_signature_facts() {
5114        let source = r#"
5115            pub trait Store {
5116                fn save(&mut self, value: Item) {
5117                    validate(value);
5118                }
5119            }
5120
5121            struct Database;
5122
5123            impl Store for Database {
5124                fn save(&mut self, value: Item) {
5125                    self.prepare();
5126                    persist(value);
5127                }
5128            }
5129        "#;
5130        let frontend = RustBackend;
5131        let analysis = frontend
5132            .analyze_file(
5133                &FileContext {
5134                    path: Path::new("src/lib.rs"),
5135                    module: &[],
5136                },
5137                source,
5138            )
5139            .unwrap();
5140
5141        assert_eq!(analysis.functions.len(), 2);
5142        let implementation = analysis
5143            .functions
5144            .iter()
5145            .find(|function| function.id.container.as_deref() == Some("Database as Store"))
5146            .unwrap();
5147        assert_eq!(implementation.calls.len(), 2);
5148        assert!(analysis.facts.iter().any(|fact| {
5149            fact.subject == implementation.id
5150                && fact.kind == "receiver"
5151                && fact.value == "&mut self"
5152        }));
5153        assert!(analysis.facts.iter().any(|fact| {
5154            fact.subject == implementation.id && fact.kind == "impl-trait" && fact.key == "Store"
5155        }));
5156    }
5157
5158    #[test]
5159    fn closure_arguments_are_labels_not_serialized_bodies() {
5160        let source = r#"
5161            fn run(values: &[String]) {
5162                values
5163                    .iter()
5164                    .filter_map(|value| {
5165                        parse(value);
5166                        Some(value)
5167                    })
5168                    .collect::<Vec<_>>();
5169            }
5170            fn parse(_: &String) {}
5171        "#;
5172        let frontend = RustBackend;
5173        let analysis = frontend
5174            .analyze_file(
5175                &FileContext {
5176                    path: Path::new("src/lib.rs"),
5177                    module: &[],
5178                },
5179                source,
5180            )
5181            .unwrap();
5182
5183        let run = analysis
5184            .functions
5185            .iter()
5186            .find(|function| function.label.default.starts_with("run("))
5187            .unwrap();
5188        let labels = run
5189            .calls
5190            .iter()
5191            .map(|call| call.label.default.as_str())
5192            .collect::<Vec<_>>();
5193
5194        assert_eq!(
5195            labels,
5196            ["values.iter()", "filter_map<λ#1>()", "collect<Vec<_>>()",]
5197        );
5198        assert!(labels.iter().all(|label| !label.contains("parse(value)")));
5199
5200        let closure = analysis
5201            .functions
5202            .iter()
5203            .find(|function| function.label.default.starts_with("λ#1("))
5204            .unwrap();
5205        assert!(
5206            closure
5207                .calls
5208                .iter()
5209                .any(|call| call.label.default == "parse(value)")
5210        );
5211    }
5212
5213    #[test]
5214    fn rustc_public_can_analyze_a_compilable_file() {
5215        let directory =
5216            std::env::temp_dir().join(format!("diffkit-rustc-public-{}", std::process::id()));
5217        fs::create_dir_all(&directory).unwrap();
5218        let path = directory.join("input.rs");
5219        fs::write(&path, "pub fn entry() { helper(); } fn helper() {}\n").unwrap();
5220
5221        let analysis = analyze_semantic_file(&path).unwrap();
5222
5223        assert!(
5224            analysis
5225                .functions
5226                .iter()
5227                .any(|function| function.id.name.ends_with("::entry"))
5228        );
5229        assert!(
5230            analysis
5231                .functions
5232                .iter()
5233                .any(|function| function.id.name.ends_with("::helper"))
5234        );
5235        fs::remove_dir_all(directory).unwrap();
5236    }
5237
5238    #[test]
5239    fn rustc_public_skips_higher_ranked_vtables() {
5240        let directory = std::env::temp_dir().join(format!(
5241            "diffkit-rustc-public-bound-vtable-{}",
5242            std::process::id()
5243        ));
5244        fs::create_dir_all(&directory).unwrap();
5245        let path = directory.join("input.rs");
5246        fs::write(
5247            &path,
5248            r#"
5249                use std::{
5250                    any::{Any, TypeId},
5251                    collections::HashMap,
5252                    sync::Arc,
5253                };
5254
5255                pub fn populate() {
5256                    let mut values: HashMap<TypeId, Arc<dyn Any + Send + Sync>> = HashMap::new();
5257                    values.insert(TypeId::of::<u8>(), Arc::new(1_u8));
5258                    values.reserve(1);
5259                }
5260            "#,
5261        )
5262        .unwrap();
5263
5264        let analysis = analyze_semantic_file(&path).unwrap();
5265
5266        assert!(
5267            analysis
5268                .functions
5269                .iter()
5270                .any(|function| function.label.default.starts_with("populate("))
5271        );
5272        fs::remove_dir_all(directory).unwrap();
5273    }
5274
5275    #[test]
5276    fn rustc_public_does_not_open_opaque_instance_arguments() {
5277        let directory = std::env::temp_dir().join(format!(
5278            "diffkit-rustc-public-opaque-argument-{}",
5279            std::process::id()
5280        ));
5281        fs::create_dir_all(&directory).unwrap();
5282        let path = directory.join("input.rs");
5283        fs::write(
5284            &path,
5285            r#"
5286                fn make() -> impl Copy { 1_u8 }
5287                fn consume<T: Copy>(_value: T) {}
5288                pub fn run() { consume(make()); }
5289            "#,
5290        )
5291        .unwrap();
5292
5293        let analysis = analyze_semantic_file(&path).unwrap();
5294
5295        assert!(
5296            analysis
5297                .functions
5298                .iter()
5299                .any(|function| function.label.default.starts_with("run("))
5300        );
5301        fs::remove_dir_all(directory).unwrap();
5302    }
5303
5304    #[test]
5305    fn semantic_analysis_omits_enum_variant_constructors() {
5306        let directory = std::env::temp_dir().join(format!(
5307            "diffkit-rustc-public-constructors-{}",
5308            std::process::id()
5309        ));
5310        fs::create_dir_all(&directory).unwrap();
5311        let path = directory.join("input.rs");
5312        let source = r#"
5313            pub struct Node;
5314            pub fn root(before: Option<&Node>, after: Option<&Node>) -> Option<()> {
5315                match (before, after) {
5316                    (Some(before), Some(after)) => Some(inner(Some(before), Some(after))),
5317                    _ => None,
5318                }
5319            }
5320            fn inner(_: Option<&Node>, _: Option<&Node>) {}
5321            pub fn is_json(value: Option<&str>) -> bool { value == Some("json") }
5322        "#;
5323        fs::write(&path, source).unwrap();
5324
5325        let analysis = analyze_semantic_file(&path).unwrap();
5326        let root = analysis
5327            .functions
5328            .iter()
5329            .find(|function| function.label.default.starts_with("root("))
5330            .unwrap();
5331        let calls = root
5332            .calls
5333            .iter()
5334            .map(|call| call.label.default.as_str())
5335            .collect::<Vec<_>>();
5336
5337        assert_eq!(calls, ["inner(Some(before), Some(after))"]);
5338        let is_json = analysis
5339            .functions
5340            .iter()
5341            .find(|function| function.label.default.starts_with("is_json("))
5342            .unwrap();
5343        assert!(is_json.calls.is_empty(), "{:#?}", is_json.calls);
5344        fs::remove_dir_all(directory).unwrap();
5345    }
5346
5347    #[test]
5348    fn semantic_labels_do_not_expose_external_inferred_generics_or_closure_bodies() {
5349        let directory = std::env::temp_dir().join(format!(
5350            "diffkit-rustc-public-call-labels-{}",
5351            std::process::id()
5352        ));
5353        fs::create_dir_all(&directory).unwrap();
5354        let path = directory.join("input.rs");
5355        fs::write(
5356            &path,
5357            r#"
5358                pub fn run(values: &[String]) -> Vec<usize> {
5359                    values
5360                        .iter()
5361                        .filter_map(|value| value.parse::<usize>().ok())
5362                        .collect()
5363                }
5364            "#,
5365        )
5366        .unwrap();
5367
5368        let analysis = analyze_semantic_file(&path).unwrap();
5369        let run = analysis
5370            .functions
5371            .iter()
5372            .find(|function| function.label.default.starts_with("run("))
5373            .unwrap();
5374        let labels = run
5375            .calls
5376            .iter()
5377            .map(|call| call.label.default.as_str())
5378            .collect::<Vec<_>>();
5379
5380        assert_eq!(labels, ["values.iter()", "filter_map<λ#1>()", "collect()"]);
5381        assert!(
5382            labels
5383                .iter()
5384                .all(|label| !label.contains("impl ") && !label.contains("|value|"))
5385        );
5386        fs::remove_dir_all(directory).unwrap();
5387    }
5388
5389    #[test]
5390    fn rustc_public_resolves_generic_trait_dispatch() {
5391        let directory = std::env::temp_dir().join(format!(
5392            "diffkit-rustc-public-generics-{}",
5393            std::process::id()
5394        ));
5395        fs::create_dir_all(&directory).unwrap();
5396        let path = directory.join("input.rs");
5397        fs::write(
5398            &path,
5399            r#"
5400                #[derive(Clone, Copy)]
5401                pub struct Order;
5402                trait Store { fn save(&self, order: Order); }
5403                struct Postgres;
5404                impl Store for Postgres {
5405                    fn save(&self, order: Order) {
5406                        sql::begin();
5407                        sql::insert(order);
5408                        sql::commit();
5409                    }
5410                }
5411                struct S3;
5412                impl Store for S3 {
5413                    fn save(&self, order: Order) {
5414                        aws::sign(order);
5415                        aws::put_object(order);
5416                    }
5417                }
5418                fn run<S: Store>(storage: &S, order: Order) {
5419                    validate(order);
5420                    storage.save(order);
5421                    finalize(order);
5422                }
5423                pub fn entry(order: Order) {
5424                    run(&Postgres, order);
5425                    run(&S3, order);
5426                }
5427                fn validate(_: Order) {}
5428                fn finalize(_: Order) {}
5429                mod sql {
5430                    use super::Order;
5431                    pub fn begin() {}
5432                    pub fn insert(_: Order) {}
5433                    pub fn commit() {}
5434                }
5435                mod aws {
5436                    use super::Order;
5437                    pub fn sign(_: Order) {}
5438                    pub fn put_object(_: Order) {}
5439                }
5440            "#,
5441        )
5442        .unwrap();
5443
5444        let analysis = analyze_semantic_file(&path).unwrap();
5445        let labels = analysis
5446            .functions
5447            .iter()
5448            .map(|function| {
5449                (
5450                    function.label.default.clone(),
5451                    function
5452                        .calls
5453                        .iter()
5454                        .map(|call| call.label.default.clone())
5455                        .collect::<Vec<_>>(),
5456                )
5457            })
5458            .collect::<Vec<_>>();
5459        assert!(labels.iter().any(|(label, calls)| {
5460            label == "run<Postgres>(storage, order)"
5461                && calls.iter().any(|call| call == "Postgres::save(order)")
5462        }));
5463        assert!(labels.iter().any(|(label, calls)| {
5464            label == "run<S3>(storage, order)" && calls.iter().any(|call| call == "S3::save(order)")
5465        }));
5466        fs::remove_dir_all(directory).unwrap();
5467    }
5468
5469    #[test]
5470    fn specialization_consumes_duplicate_normalized_instances() {
5471        let span = SourceSpan {
5472            file: PathBuf::from("src/lib.rs"),
5473            start_line: 1,
5474            start_column: 1,
5475            start_byte: None,
5476            end_line: 1,
5477            end_column: 1,
5478            end_byte: None,
5479        };
5480        let function = || RawSemanticFunction {
5481            key: "crate::duplicate".to_owned(),
5482            display: "duplicate".to_owned(),
5483            definition_name: "crate::duplicate".to_owned(),
5484            body_span: span.clone(),
5485            calls: Vec::new(),
5486            constructor_spans: Vec::new(),
5487            constructor_names: Vec::new(),
5488            parameter_types: Vec::new(),
5489        };
5490
5491        let specialized =
5492            specialize_semantic_functions(&[function(), function()], &[], &HashMap::new(), "crate");
5493
5494        assert_eq!(specialized.len(), 1);
5495        assert_eq!(specialized[0].key, "crate::duplicate");
5496    }
5497
5498    #[test]
5499    fn closure_locations_survive_until_the_source_lambda_mapping() {
5500        assert_eq!(
5501            normalize_instance_key("core::result::Result::map::<{closure@src/lib.rs:10:4: 10:12}>"),
5502            "core::result::Result::map<{closure@src/lib.rs:10:4: 10:12}>"
5503        );
5504        assert_eq!(
5505            normalize_type_display(
5506                "diffkit::Runner<{async block@src/lib.rs:20:8: 24:9}>",
5507                "diffkit"
5508            ),
5509            "Runner<{async block}>"
5510        );
5511    }
5512}