Skip to main content

diffkit/language/
ocaml.rs

1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet, VecDeque};
3use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::sync::atomic::{AtomicU64, Ordering};
8
9use sha2::{Digest, Sha256};
10use tree_sitter::{Node, Parser, Tree};
11
12use super::{FileContext, FrontendResult, LanguageBackend, ProjectContext};
13use crate::model::{
14    CallLabel, CallSite, CallSiteId, CallSyntax, CallTarget, DispatchCandidate, DispatchEvidence,
15    DispatchResolution, FileAnalysis, FunctionInfo, LanguageFact, LanguageId, SourceSpan, SymbolId,
16    UnresolvedReason,
17};
18use crate::source::{collect_files_with_extension, collect_source_files, paths_match};
19
20static OCAML_TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
21const OCAML_EXTRACTOR_SOURCE: &str = include_str!("../../support/ocaml/extract.ml");
22
23thread_local! {
24    /// Expression flow performs many small parses during specialization. A
25    /// parser is reusable after each returned `Tree`, so keep one per worker
26    /// thread instead of rebuilding its language tables for every argument.
27    static OCAML_EXPRESSION_PARSER: RefCell<Option<Parser>> = RefCell::new({
28        let mut parser = Parser::new();
29        parser
30            .set_language(&tree_sitter_ocaml::LANGUAGE_OCAML.into())
31            .ok()
32            .map(|_| parser)
33    });
34}
35
36fn parse_ocaml_expression(source: &str) -> Option<Tree> {
37    OCAML_EXPRESSION_PARSER.with(|parser| parser.borrow_mut().as_mut()?.parse(source, None))
38}
39
40/// OCaml's source-label stage. Dune projects overlay compiler-libs Typedtree
41/// paths in `analyze_semantic_project`; standalone source sets retain
42/// conservative module/local resolution and the same graph contract.
43#[derive(Default)]
44pub struct OcamlFrontend;
45
46pub static OCAML_BACKEND: OcamlFrontend = OcamlFrontend;
47
48/// Analyze a Dune project from compiler-generated `.cmt` Typedtrees. Source
49/// parsing remains responsible for language-shaped labels; resolved `Path.t`
50/// values from compiler-libs are authoritative for call identity.
51pub fn analyze_semantic_project(root: &Path) -> FrontendResult<FileAnalysis> {
52    let root = root.canonicalize()?;
53    let build = Command::new("dune")
54        .args(["build", "@check", "--display", "quiet"])
55        .current_dir(&root)
56        .output()
57        .map_err(|error| {
58            io::Error::new(
59                error.kind(),
60                format!("failed to run Dune for OCaml semantic analysis: {error}"),
61            )
62        })?;
63    if !build.status.success() {
64        let diagnostic = String::from_utf8_lossy(&build.stderr).trim().to_owned();
65        return Err(io::Error::other(if diagnostic.is_empty() {
66            format!("Dune semantic build exited with {}", build.status)
67        } else {
68            diagnostic
69        })
70        .into());
71    }
72
73    let helper = OcamlExtractor::compile()?;
74    let cmt_files = collect_files_with_extension(&root.join("_build"), "cmt")?;
75    let mut events = Vec::new();
76    for cmt in cmt_files {
77        let output = Command::new(helper.executable()).arg(&cmt).output()?;
78        if !output.status.success() {
79            let diagnostic = String::from_utf8_lossy(&output.stderr).trim().to_owned();
80            return Err(io::Error::other(format!(
81                "compiler-libs could not read {}: {}",
82                cmt.display(),
83                diagnostic
84            ))
85            .into());
86        }
87        events.extend(parse_typed_events(
88            &root,
89            &String::from_utf8(output.stdout)?,
90        )?);
91    }
92
93    let source_files = collect_source_files(&root, &["ml", "mli"])?;
94    let mut analysis = FileAnalysis::default();
95    for file in source_files {
96        let source = fs::read_to_string(&file)?;
97        let module = ocaml_file_module_path(&root, &file);
98        let mut file_analysis = analyze_ocaml_syntax(
99            &FileContext {
100                path: &file,
101                module: &module,
102            },
103            &source,
104        )?;
105        apply_typed_events(&mut file_analysis, &events);
106        analysis.append(file_analysis);
107    }
108    resolve_ocaml_function_values(&mut analysis);
109    Ok(analysis)
110}
111
112/// Analyze an OCaml source set without a Dune project. This is used for
113/// standalone source trees; Dune projects use `analyze_semantic_project`.
114pub fn analyze_source_project(root: &Path) -> FrontendResult<FileAnalysis> {
115    let root = root.canonicalize()?;
116    let files = collect_source_files(&root, &["ml", "mli"])?;
117    let mut analysis = FileAnalysis::default();
118    for file in files {
119        let source = fs::read_to_string(&file)?;
120        let module = ocaml_file_module_path(&root, &file);
121        let mut file_analysis = analyze_ocaml_syntax(
122            &FileContext {
123                path: &file,
124                module: &module,
125            },
126            &source,
127        )?;
128        if let Some(events) = standalone_typed_events(&file, &source)? {
129            apply_typed_events(&mut file_analysis, &events);
130        }
131        analysis.append(file_analysis);
132    }
133    resolve_ocaml_function_values(&mut analysis);
134    Ok(analysis)
135}
136
137struct OcamlExtractor {
138    directory: PathBuf,
139}
140
141impl OcamlExtractor {
142    fn compile() -> FrontendResult<Self> {
143        let sequence = OCAML_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
144        let directory = std::env::temp_dir().join(format!(
145            "diffkit-ocaml-extractor-{}-{sequence}",
146            std::process::id()
147        ));
148        fs::create_dir(&directory)?;
149        let extractor = Self { directory };
150        let source = extractor.directory.join("extract.ml");
151        fs::write(&source, OCAML_EXTRACTOR_SOURCE)?;
152        let output = Command::new("ocamlc")
153            .args(["-I", "+compiler-libs", "ocamlcommon.cma"])
154            .arg(&source)
155            .arg("-o")
156            .arg(extractor.executable())
157            .output()
158            .map_err(|error| {
159                io::Error::new(
160                    error.kind(),
161                    format!("failed to start ocamlc for compiler-libs adapter: {error}"),
162                )
163            })?;
164        if !output.status.success() {
165            let diagnostic = String::from_utf8_lossy(&output.stderr).trim().to_owned();
166            return Err(io::Error::other(format!(
167                "failed to compile compiler-libs adapter: {diagnostic}"
168            ))
169            .into());
170        }
171        Ok(extractor)
172    }
173
174    fn executable(&self) -> PathBuf {
175        self.directory.join("extract")
176    }
177}
178
179impl Drop for OcamlExtractor {
180    fn drop(&mut self) {
181        let _ = fs::remove_dir_all(&self.directory);
182    }
183}
184
185#[derive(Clone, Debug)]
186struct TypedCallEvent {
187    target: Option<String>,
188    signature: Option<String>,
189    span: SourceSpan,
190}
191
192impl LanguageBackend for OcamlFrontend {
193    fn language(&self) -> LanguageId {
194        LanguageId::new("ocaml")
195    }
196
197    fn extensions(&self) -> &'static [&'static str] {
198        &["ml", "mli"]
199    }
200
201    fn analyze_file(
202        &self,
203        context: &FileContext<'_>,
204        source: &str,
205    ) -> FrontendResult<FileAnalysis> {
206        let mut analysis = analyze_ocaml_syntax(context, source)?;
207        if let Some(events) = standalone_typed_events(context.path, source)? {
208            apply_typed_events(&mut analysis, &events);
209        }
210        resolve_ocaml_function_values(&mut analysis);
211        Ok(analysis)
212    }
213
214    fn analyze_project(&self, context: &ProjectContext<'_>) -> FrontendResult<FileAnalysis> {
215        if context.root.join("dune-project").is_file() {
216            analyze_semantic_project(context.root)
217        } else {
218            analyze_source_project(context.root)
219        }
220    }
221}
222
223fn standalone_typed_events(
224    source_path: &Path,
225    source: &str,
226) -> FrontendResult<Option<Vec<TypedCallEvent>>> {
227    if Command::new("ocamlc").arg("-version").output().is_err() {
228        return Ok(None);
229    }
230    let sequence = OCAML_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
231    let directory = std::env::temp_dir().join(format!(
232        "diffkit-ocaml-source-{}-{sequence}",
233        std::process::id()
234    ));
235    fs::create_dir(&directory)?;
236    let capture = StandaloneOcamlCapture { directory };
237    let extension = source_path
238        .extension()
239        .and_then(|extension| extension.to_str())
240        .unwrap_or("ml");
241    let temporary_source = capture.directory.join(format!("input.{extension}"));
242    fs::write(&temporary_source, source)?;
243    let compile = Command::new("ocamlc")
244        .args(["-bin-annot", "-c"])
245        .arg(&temporary_source)
246        .current_dir(&capture.directory)
247        .output()?;
248    if !compile.status.success() {
249        // A standalone snippet may intentionally depend on surrounding
250        // modules. Keep the conservative graph, with unresolved calls marked
251        // explicitly, instead of pretending the compiler accepted it.
252        return Ok(None);
253    }
254    let annotation = if extension == "mli" {
255        capture.directory.join("input.cmti")
256    } else {
257        capture.directory.join("input.cmt")
258    };
259    if !annotation.is_file() {
260        return Ok(None);
261    }
262    let helper = OcamlExtractor::compile()?;
263    let output = Command::new(helper.executable()).arg(annotation).output()?;
264    if !output.status.success() {
265        return Ok(None);
266    }
267    let mut events = parse_typed_events(&capture.directory, &String::from_utf8(output.stdout)?)?;
268    for event in &mut events {
269        event.span.file = source_path.to_path_buf();
270    }
271    Ok(Some(events))
272}
273
274struct StandaloneOcamlCapture {
275    directory: PathBuf,
276}
277
278impl Drop for StandaloneOcamlCapture {
279    fn drop(&mut self) {
280        let _ = fs::remove_dir_all(&self.directory);
281    }
282}
283
284fn analyze_ocaml_syntax(context: &FileContext<'_>, source: &str) -> FrontendResult<FileAnalysis> {
285    let mut parser = Parser::new();
286    let language = if context.path.extension().and_then(|value| value.to_str()) == Some("mli") {
287        tree_sitter_ocaml::LANGUAGE_OCAML_INTERFACE
288    } else {
289        tree_sitter_ocaml::LANGUAGE_OCAML
290    };
291    parser
292        .set_language(&language.into())
293        .map_err(|error| io::Error::other(format!("failed to load OCaml grammar: {error}")))?;
294    let tree = parser
295        .parse(source, None)
296        .ok_or_else(|| io::Error::other("OCaml parser returned no syntax tree"))?;
297    if tree.root_node().has_error() {
298        return Err(io::Error::other(format!(
299            "failed to parse OCaml source: {}",
300            context.path.display()
301        ))
302        .into());
303    }
304
305    let mut analysis = FileAnalysis::default();
306    analysis.source_files.insert(context.path.to_path_buf());
307    let module = context
308        .module
309        .iter()
310        .map(|part| ocaml_module_name(part))
311        .collect::<Vec<_>>();
312    analyze_structure(
313        context.path,
314        &module,
315        tree.root_node(),
316        source,
317        &mut analysis,
318    );
319    disambiguate_ocaml_shadowed_bindings(&mut analysis);
320    resolve_ocaml_recursive_groups(&mut analysis);
321    resolve_local_callables(&mut analysis);
322    redact_anonymous_call_arguments(&mut analysis);
323    Ok(analysis)
324}
325
326#[derive(Clone)]
327struct OcamlBindingRemap {
328    old: SymbolId,
329    new: SymbolId,
330    owner_span: SourceSpan,
331}
332
333/// OCaml permits a later top-level `let` to shadow an earlier binding with the
334/// same source path. `Path.name` deliberately omits compiler-local stamps, so
335/// preserve that identity here before Typedtree call events are overlaid.
336fn disambiguate_ocaml_shadowed_bindings(analysis: &mut FileAnalysis) {
337    let mut groups = BTreeMap::<(PathBuf, Vec<String>, String), Vec<usize>>::new();
338    for (index, function) in analysis.functions.iter().enumerate() {
339        if function.id.container.is_none() {
340            groups
341                .entry((
342                    function.span.file.clone(),
343                    function.id.module.clone(),
344                    function.id.name.clone(),
345                ))
346                .or_default()
347                .push(index);
348        }
349    }
350
351    let mut remaps = Vec::new();
352    for indices in groups.values_mut().filter(|indices| indices.len() > 1) {
353        indices.sort_by_key(|index| {
354            let function = &analysis.functions[*index];
355            (
356                function.span.start_line,
357                function.span.start_column,
358                function.span.end_line,
359                function.span.end_column,
360            )
361        });
362        for (ordinal, index) in indices.iter().copied().enumerate() {
363            let owner = analysis.functions[index].clone();
364            let old_owner_text = owner.id.to_string();
365            let mut new_owner = owner.id.clone();
366            if ordinal + 1 != indices.len() {
367                new_owner.name = format!("{}#binding{ordinal}", owner.id.name);
368            }
369            let new_owner_text = new_owner.to_string();
370
371            for function in &mut analysis.functions {
372                if function.span.file != owner.span.file || !owner.span.contains(&function.span) {
373                    continue;
374                }
375                let old = function.id.clone();
376                if old == owner.id {
377                    function.id = new_owner.clone();
378                } else if let Some(container) = &old.container
379                    && container.contains(&old_owner_text)
380                {
381                    function.id.container =
382                        Some(container.replacen(&old_owner_text, &new_owner_text, 1));
383                } else {
384                    continue;
385                }
386                remaps.push(OcamlBindingRemap {
387                    old,
388                    new: function.id.clone(),
389                    owner_span: owner.span.clone(),
390                });
391            }
392        }
393    }
394
395    for fact in &mut analysis.facts {
396        if let Some(remap) = remaps.iter().find(|remap| {
397            fact.subject == remap.old
398                && fact.span.file == remap.owner_span.file
399                && remap.owner_span.contains(&fact.span)
400        }) {
401            fact.subject = remap.new.clone();
402        }
403    }
404
405    analysis.roots = analysis
406        .roots
407        .iter()
408        .map(|root| {
409            remaps
410                .iter()
411                .find(|remap| remap.old == *root)
412                .map_or_else(|| root.clone(), |remap| remap.new.clone())
413        })
414        .collect();
415}
416
417fn resolve_ocaml_recursive_groups(analysis: &mut FileAnalysis) {
418    let groups = analysis
419        .facts
420        .iter()
421        .filter(|fact| fact.kind == "recursive-group")
422        .fold(
423            BTreeMap::<(PathBuf, String), Vec<SymbolId>>::new(),
424            |mut groups, fact| {
425                groups
426                    .entry((fact.span.file.clone(), fact.value.clone()))
427                    .or_default()
428                    .push(fact.subject.clone());
429                groups
430            },
431        );
432    let function_index = analysis.functions.clone();
433    for members in groups.values() {
434        let owners = members
435            .iter()
436            .filter_map(|member| {
437                function_index
438                    .iter()
439                    .find(|function| function.id == *member)
440            })
441            .collect::<Vec<_>>();
442        for function in &mut analysis.functions {
443            if !owners.iter().any(|owner| {
444                owner.span.file == function.span.file && owner.span.contains(&function.span)
445            }) {
446                continue;
447            }
448            for call in &mut function.calls {
449                let CallSyntax::Path(parts) = call.syntax.visible() else {
450                    continue;
451                };
452                let Some(name) = parts.last() else {
453                    continue;
454                };
455                let mut targets = owners
456                    .iter()
457                    .filter(|owner| {
458                        local_callable_source_name(owner) == name
459                            && (parts.len() == 1 || parts[..parts.len() - 1] == owner.id.module)
460                    })
461                    .map(|owner| owner.id.clone())
462                    .collect::<Vec<_>>();
463                targets.sort();
464                targets.dedup();
465                if targets.len() == 1 {
466                    call.target = CallTarget::Direct(targets.remove(0));
467                }
468            }
469        }
470    }
471}
472
473fn ocaml_file_module_path(root: &Path, file: &Path) -> Vec<String> {
474    let relative = file.strip_prefix(root).unwrap_or(file);
475    relative
476        .file_stem()
477        .and_then(|stem| stem.to_str())
478        .map(|stem| vec![stem.to_owned()])
479        .unwrap_or_default()
480}
481
482fn parse_typed_events(root: &Path, output: &str) -> FrontendResult<Vec<TypedCallEvent>> {
483    output
484        .lines()
485        .filter(|line| !line.trim().is_empty())
486        .map(|line| {
487            let fields = line.split('\t').collect::<Vec<_>>();
488            if fields.len() != 8 {
489                return Err(
490                    io::Error::other(format!("invalid compiler-libs event: {line}")).into(),
491                );
492            }
493            let file = PathBuf::from(fields[2]);
494            let file = if file.is_absolute() {
495                file
496            } else {
497                root.join(file)
498            };
499            let file = dune_source_path(root, &file).unwrap_or(file);
500            Ok(TypedCallEvent {
501                target: (fields[0] == "direct").then(|| fields[1].to_owned()),
502                signature: (!fields[7].is_empty()).then(|| fields[7].to_owned()),
503                span: SourceSpan {
504                    file,
505                    start_line: fields[3].parse()?,
506                    start_column: fields[4].parse()?,
507                    start_byte: None,
508                    end_line: fields[5].parse()?,
509                    end_column: fields[6].parse()?,
510                    end_byte: None,
511                },
512            })
513        })
514        .collect()
515}
516
517fn dune_source_path(root: &Path, file: &Path) -> Option<PathBuf> {
518    let relative = file
519        .strip_prefix(root.join("_build"))
520        .ok()
521        .or_else(|| file.strip_prefix("_build").ok())?;
522    let components = relative.components().collect::<Vec<_>>();
523    let skip = if components
524        .first()
525        .is_some_and(|component| component.as_os_str() == ".sandbox")
526    {
527        3
528    } else {
529        1
530    };
531    let candidate = components
532        .get(skip..)?
533        .iter()
534        .fold(root.to_path_buf(), |path, component| {
535            path.join(component.as_os_str())
536        });
537    candidate.is_file().then_some(candidate)
538}
539
540fn apply_typed_events(analysis: &mut FileAnalysis, events: &[TypedCallEvent]) {
541    let parameters = ocaml_parameters(&analysis.functions, &analysis.facts);
542    let local_values = ocaml_local_values(&analysis.facts);
543    let local_symbols = analysis
544        .functions
545        .iter()
546        .map(|function| function.id.clone())
547        .collect::<BTreeSet<_>>();
548    let mut claimed = BTreeSet::new();
549    for function in &mut analysis.functions {
550        for call in &mut function.calls {
551            if matches!(
552                &call.target,
553                CallTarget::Direct(target) if local_symbols.contains(target)
554            ) {
555                continue;
556            }
557            if matches!(call.target, CallTarget::Dynamic { .. }) {
558                continue;
559            }
560            let Some((event_index, event)) = events
561                .iter()
562                .enumerate()
563                .filter(|(index, _)| !claimed.contains(index))
564                .filter(|event| {
565                    ocaml_source_files_match(&event.1.span.file, &call.span.file)
566                        && event.1.span.overlaps(&call.span)
567                })
568                .min_by_key(|(_, event)| event.span.boundary_distance(&call.span))
569            else {
570                continue;
571            };
572            claimed.insert(event_index);
573            if let Some(signature) = &event.signature {
574                call.label.typed = Some(annotate_ocaml_call(&call.label.default, call, signature));
575            }
576            let is_flow_resolved_callable = match call.syntax.visible() {
577                CallSyntax::Path(parts) => {
578                    parameters.get(&function.id).is_some_and(|bindings| {
579                        called_ocaml_parameter_index(parts, bindings, Some(&call.span)).is_some()
580                    }) || (parts.len() == 1
581                        && unique_ocaml_local_value(
582                            &local_values,
583                            &function.id,
584                            &parts[0],
585                            Some(&call.span),
586                        )
587                        .is_some())
588                }
589                _ => false,
590            };
591            call.target = match &event.target {
592                _ if is_flow_resolved_callable => CallTarget::Unresolved,
593                Some(target) => CallTarget::Direct(ocaml_path_symbol(target)),
594                None => CallTarget::Indirect {
595                    signature: event.signature.clone(),
596                    reason: UnresolvedReason::OpaqueInput,
597                },
598            };
599        }
600    }
601}
602
603fn annotate_ocaml_call(label: &str, call: &CallSite, signature: &str) -> String {
604    let arguments = ocaml_call_arguments(call);
605    if arguments.is_empty() {
606        return label.to_owned();
607    }
608    let mut types = split_ocaml_function_type(signature);
609    if types.len() <= 1 {
610        return label.to_owned();
611    }
612    types.pop(); // Return type is intentionally not rendered.
613    if types.len() < arguments.len() {
614        return label.to_owned();
615    }
616    let callee = match call.syntax.visible() {
617        CallSyntax::Path(parts) => parts.join("."),
618        CallSyntax::SelfMethod(method) => method.clone(),
619        CallSyntax::Method { receiver, method } => format!("{receiver}#{method}"),
620        CallSyntax::Expression(expression) => expression.clone(),
621        CallSyntax::CompilerConfirmed(syntax) => syntax.key_fragment(),
622    };
623    let types = &types[types.len() - arguments.len()..];
624    let annotated = arguments
625        .iter()
626        .zip(types)
627        .map(|(argument, ty)| {
628            let ty = ty.split_once(':').map_or(ty.as_str(), |(_, ty)| ty).trim();
629            format!("({argument}: {ty})")
630        })
631        .collect::<Vec<_>>()
632        .join(" ");
633    format!("{callee} {annotated}")
634}
635
636fn split_ocaml_function_type(signature: &str) -> Vec<String> {
637    let mut result = Vec::new();
638    let mut start = 0usize;
639    let mut depth = 0usize;
640    let bytes = signature.as_bytes();
641    let mut index = 0usize;
642    while index + 1 < bytes.len() {
643        match bytes[index] {
644            b'(' | b'[' | b'{' => depth += 1,
645            b')' | b']' | b'}' => depth = depth.saturating_sub(1),
646            b'-' if bytes[index + 1] == b'>' && depth == 0 => {
647                result.push(signature[start..index].trim().to_owned());
648                start = index + 2;
649                index += 1;
650            }
651            _ => {}
652        }
653        index += 1;
654    }
655    result.push(signature[start..].trim().to_owned());
656    result
657}
658
659fn ocaml_path_symbol(path: &str) -> SymbolId {
660    let mut parts = path
661        .trim_start_matches("global ")
662        .split('.')
663        .filter(|part| !part.is_empty())
664        .map(|part| part.rsplit("__").next().unwrap_or(part).to_owned())
665        .collect::<Vec<_>>();
666    let name = parts.pop().unwrap_or_else(|| path.to_owned());
667    SymbolId {
668        language: LanguageId::new("ocaml"),
669        module: parts,
670        container: None,
671        name,
672    }
673}
674
675fn ocaml_source_files_match(left: &Path, right: &Path) -> bool {
676    paths_match(left, right)
677}
678
679fn ocaml_module_name(value: &str) -> String {
680    let mut characters = value.chars();
681    let Some(first) = characters.next() else {
682        return String::new();
683    };
684    first.to_uppercase().chain(characters).collect()
685}
686
687fn analyze_structure(
688    file: &Path,
689    module: &[String],
690    structure: Node<'_>,
691    source: &str,
692    analysis: &mut FileAnalysis,
693) {
694    let mut cursor = structure.walk();
695    for child in structure.named_children(&mut cursor) {
696        match child.kind() {
697            "module_definition" => analyze_module_definition(file, module, child, source, analysis),
698            "value_definition" => analyze_value_definition(file, module, child, source, analysis),
699            _ => {}
700        }
701    }
702}
703
704fn analyze_module_definition(
705    file: &Path,
706    module: &[String],
707    definition: Node<'_>,
708    source: &str,
709    analysis: &mut FileAnalysis,
710) {
711    let mut cursor = definition.walk();
712    for binding in definition
713        .named_children(&mut cursor)
714        .filter(|child| child.kind() == "module_binding")
715    {
716        let Some(name_node) = direct_named_child(binding, "module_name") else {
717            continue;
718        };
719        let Some(body) = binding.child_by_field_name("body") else {
720            continue;
721        };
722        if body.kind() == "module_path" {
723            analysis.facts.push(LanguageFact {
724                subject: SymbolId {
725                    language: LanguageId::new("ocaml"),
726                    module: module.to_vec(),
727                    container: None,
728                    name: node_text(name_node, source).to_owned(),
729                },
730                namespace: LanguageId::new("ocaml"),
731                kind: "module-alias".to_owned(),
732                key: "target".to_owned(),
733                value: normalize_source(node_text(body, source)),
734                span: tree_sitter_span(file, binding),
735            });
736            continue;
737        }
738        if body.kind() == "module_application" {
739            let Some((functor, arguments)) = ocaml_module_application(body, source) else {
740                continue;
741            };
742            analysis.facts.push(LanguageFact {
743                subject: SymbolId {
744                    language: LanguageId::new("ocaml"),
745                    module: module.to_vec(),
746                    container: None,
747                    name: node_text(name_node, source).to_owned(),
748                },
749                namespace: LanguageId::new("ocaml"),
750                kind: "functor-instance".to_owned(),
751                key: functor,
752                value: arguments.join("\t"),
753                span: tree_sitter_span(file, binding),
754            });
755            continue;
756        }
757        for (index, parameter) in direct_named_children(binding, "module_parameter")
758            .into_iter()
759            .enumerate()
760        {
761            let Some(parameter_name) = direct_named_child(parameter, "module_name") else {
762                continue;
763            };
764            analysis.facts.push(LanguageFact {
765                subject: SymbolId {
766                    language: LanguageId::new("ocaml"),
767                    module: module.to_vec(),
768                    container: None,
769                    name: node_text(name_node, source).to_owned(),
770                },
771                namespace: LanguageId::new("ocaml"),
772                kind: "functor-parameter".to_owned(),
773                key: index.to_string(),
774                value: node_text(parameter_name, source).to_owned(),
775                span: tree_sitter_span(file, parameter),
776            });
777        }
778        let body = if body.kind() == "structure" {
779            body
780        } else if let Some(structure) = first_descendant_of_kind(body, "structure") {
781            structure
782        } else {
783            continue;
784        };
785
786        let mut nested_module = module.to_vec();
787        nested_module.push(node_text(name_node, source).to_owned());
788        analyze_structure(file, &nested_module, body, source, analysis);
789    }
790}
791
792fn ocaml_module_application(node: Node<'_>, source: &str) -> Option<(String, Vec<String>)> {
793    if node.kind() == "module_path" {
794        return Some((normalize_source(node_text(node, source)), Vec::new()));
795    }
796    if node.kind() != "module_application" {
797        return None;
798    }
799    let functor = node.child_by_field_name("functor")?;
800    let (functor, mut arguments) = ocaml_module_application(functor, source)?;
801    let argument = node.child_by_field_name("argument")?;
802    let argument = if argument.kind() == "module_path" {
803        argument
804    } else {
805        first_descendant_of_kind(argument, "module_path")?
806    };
807    arguments.push(normalize_source(node_text(argument, source)));
808    Some((functor, arguments))
809}
810
811fn first_descendant_of_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
812    let mut cursor = node.walk();
813    for child in node.named_children(&mut cursor) {
814        if child.kind() == kind {
815            return Some(child);
816        }
817        if let Some(found) = first_descendant_of_kind(child, kind) {
818            return Some(found);
819        }
820    }
821    None
822}
823
824fn analyze_value_definition(
825    file: &Path,
826    module: &[String],
827    definition: Node<'_>,
828    source: &str,
829    analysis: &mut FileAnalysis,
830) {
831    let recursive_group = normalize_source(node_text(definition, source))
832        .starts_with("let rec ")
833        .then(|| {
834            format!(
835                "{}:{}-{}:{}",
836                definition.start_position().row,
837                definition.start_position().column,
838                definition.end_position().row,
839                definition.end_position().column,
840            )
841        });
842    let mut cursor = definition.walk();
843    for binding in definition
844        .named_children(&mut cursor)
845        .filter(|child| child.kind() == "let_binding")
846    {
847        if let Some(function) = function_from_binding(file, module, binding, source, analysis) {
848            if let Some(group) = &recursive_group {
849                analysis.facts.push(LanguageFact {
850                    subject: function.id.clone(),
851                    namespace: LanguageId::new("ocaml"),
852                    kind: "recursive-group".to_owned(),
853                    key: local_callable_source_name(&function).to_owned(),
854                    value: group.clone(),
855                    span: function.span.clone(),
856                });
857            }
858            let owner = function.id.clone();
859            analysis.functions.push(function);
860            collect_nested_callables_from_binding(file, module, &owner, binding, source, analysis);
861        }
862    }
863}
864
865#[allow(clippy::too_many_arguments)]
866fn collect_nested_callables(
867    file: &Path,
868    module: &[String],
869    owner: &SymbolId,
870    node: Node<'_>,
871    source: &str,
872    analysis: &mut FileAnalysis,
873    ordinal: &mut usize,
874) {
875    if node.kind() == "let_binding" {
876        let facts_start = analysis.facts.len();
877        if let Some(mut function) = function_from_binding(file, module, node, source, analysis) {
878            let display_name = function.id.name.clone();
879            let old_id = function.id.clone();
880            let current_ordinal = *ordinal;
881            *ordinal += 1;
882            function.id.container = Some(owner.to_string());
883            function.id.name = format!("{display_name}#closure{current_ordinal}");
884            function.label = local_callable_label(&function.label, &display_name, current_ordinal);
885            for fact in &mut analysis.facts[facts_start..] {
886                if fact.subject == old_id {
887                    fact.subject = function.id.clone();
888                }
889            }
890            if let Some(scope) = ocaml_local_callable_scope(node, source) {
891                analysis.facts.push(LanguageFact {
892                    subject: function.id.clone(),
893                    namespace: LanguageId::new("ocaml"),
894                    kind: "local-callable-scope".to_owned(),
895                    key: display_name.clone(),
896                    value: String::new(),
897                    span: tree_sitter_span(file, scope),
898                });
899            }
900            let nested_owner = function.id.clone();
901            analysis.functions.push(function);
902            collect_nested_callables_from_binding(
903                file,
904                module,
905                &nested_owner,
906                node,
907                source,
908                analysis,
909            );
910            return;
911        }
912    }
913
914    if matches!(node.kind(), "fun_expression" | "function_expression") {
915        let current_ordinal = *ordinal;
916        *ordinal += 1;
917        let parameters = if node.kind() == "fun_expression" {
918            direct_named_children(node, "parameter")
919        } else {
920            Vec::new()
921        };
922        let parameter_labels = if parameters.is_empty() {
923            vec!["_".to_owned()]
924        } else {
925            parameters
926                .iter()
927                .map(|parameter| parameter_default_label(*parameter, source))
928                .collect()
929        };
930        let body = node.child_by_field_name("body").unwrap_or(node);
931        let expression = node_text(node, source);
932        let base = anonymous_callable_base(expression)
933            .unwrap_or_else(|| anonymous_callable_identity(expression));
934        let id = SymbolId {
935            language: LanguageId::new("ocaml"),
936            module: module.to_vec(),
937            container: Some(owner.to_string()),
938            name: format!("{base}#closure{current_ordinal}"),
939        };
940        analysis.facts.push(LanguageFact {
941            subject: id.clone(),
942            namespace: LanguageId::new("ocaml"),
943            kind: "closure-expression".to_owned(),
944            key: base,
945            value: normalize_source(node_text(node, source)),
946            span: tree_sitter_span(file, node),
947        });
948        for (index, parameter) in parameters.iter().enumerate() {
949            analysis.facts.push(LanguageFact {
950                subject: id.clone(),
951                namespace: LanguageId::new("ocaml"),
952                kind: "parameter".to_owned(),
953                key: parameter_labels[index].clone(),
954                value: parameter_pattern_and_type(*parameter)
955                    .1
956                    .map(|node| normalize_source(node_text(node, source)))
957                    .unwrap_or_default(),
958                span: tree_sitter_span(file, *parameter),
959            });
960        }
961        collect_ocaml_parameter_refinement_facts(
962            file,
963            &id,
964            body,
965            source,
966            true,
967            &mut analysis.facts,
968        );
969        let mut calls = Vec::new();
970        collect_calls(file, body, source, true, &mut calls);
971        let lambda = format!("λ#{}", current_ordinal + 1);
972        let label = if parameter_labels.is_empty() {
973            lambda
974        } else {
975            format!("{lambda} {}", parameter_labels.join(" "))
976        };
977        let nested_owner = id.clone();
978        analysis.functions.push(FunctionInfo {
979            id,
980            label: CallLabel::new(label),
981            public: false,
982            calls,
983            span: tree_sitter_span(file, node),
984        });
985
986        let mut nested_ordinal = 0usize;
987        let mut cursor = body.walk();
988        for child in body.named_children(&mut cursor) {
989            collect_nested_callables(
990                file,
991                module,
992                &nested_owner,
993                child,
994                source,
995                analysis,
996                &mut nested_ordinal,
997            );
998        }
999        return;
1000    }
1001
1002    let mut cursor = node.walk();
1003    for child in node.named_children(&mut cursor) {
1004        collect_nested_callables(file, module, owner, child, source, analysis, ordinal);
1005    }
1006}
1007
1008fn ocaml_local_callable_scope<'tree>(binding: Node<'tree>, source: &str) -> Option<Node<'tree>> {
1009    let definition = binding
1010        .parent()
1011        .filter(|node| node.kind() == "value_definition")?;
1012    let expression = definition
1013        .parent()
1014        .filter(|node| node.kind() == "let_expression")?;
1015    if normalize_source(node_text(definition, source)).starts_with("let rec ") {
1016        Some(expression)
1017    } else {
1018        expression.child_by_field_name("body")
1019    }
1020}
1021
1022fn collect_nested_callables_from_binding(
1023    file: &Path,
1024    module: &[String],
1025    owner: &SymbolId,
1026    binding: Node<'_>,
1027    source: &str,
1028    analysis: &mut FileAnalysis,
1029) {
1030    let Some(mut body) = binding.child_by_field_name("body") else {
1031        return;
1032    };
1033    while body.kind() == "fun_expression" {
1034        let Some(next) = body.child_by_field_name("body") else {
1035            return;
1036        };
1037        body = next;
1038    }
1039    let mut ordinal = 0usize;
1040    if body.kind() == "function_expression" {
1041        let mut cursor = body.walk();
1042        for child in body.named_children(&mut cursor) {
1043            collect_nested_callables(file, module, owner, child, source, analysis, &mut ordinal);
1044        }
1045    } else {
1046        collect_nested_callables(file, module, owner, body, source, analysis, &mut ordinal);
1047    }
1048}
1049
1050fn anonymous_callable_base(expression: &str) -> Option<String> {
1051    let expression = normalize_ocaml_callable_argument(expression);
1052    if !expression.starts_with("fun ") && expression != "fun" && !expression.starts_with("function")
1053    {
1054        return None;
1055    }
1056    Some(anonymous_callable_identity(expression))
1057}
1058
1059fn anonymous_callable_identity(expression: &str) -> String {
1060    let normalized = expression.split_whitespace().collect::<Vec<_>>().join(" ");
1061    let mut hasher = Sha256::new();
1062    hasher.update(normalized.as_bytes());
1063    let digest = format!("{:x}", hasher.finalize());
1064    format!("{{lambda:{}}}", &digest[..12])
1065}
1066
1067fn local_callable_label(label: &CallLabel, name: &str, ordinal: usize) -> CallLabel {
1068    let rewrite = |text: &str| {
1069        let arguments = text
1070            .find(char::is_whitespace)
1071            .map_or("", |start| &text[start..]);
1072        format!("{name}{arguments} [closure#{ordinal}]")
1073    };
1074    CallLabel {
1075        default: rewrite(&label.default),
1076        typed: label.typed.as_deref().map(rewrite),
1077    }
1078}
1079
1080fn resolve_local_callables(analysis: &mut FileAnalysis) {
1081    let functions = analysis.functions.clone();
1082    let symbols = OcamlSymbols::new(&functions);
1083    let scopes = ocaml_local_callable_scopes(&analysis.facts);
1084    for function in &mut analysis.functions {
1085        for call in &mut function.calls {
1086            if !matches!(call.target, CallTarget::Unresolved) {
1087                continue;
1088            }
1089            if let CallSyntax::Expression(expression) = call.syntax.visible()
1090                && let Some(candidate) = anonymous_callable_symbol_at(
1091                    expression,
1092                    &function.id,
1093                    &symbols,
1094                    Some(&call.span),
1095                )
1096                && let Some(target) = symbols.get(&candidate)
1097            {
1098                let arguments = ocaml_call_arguments(call);
1099                call.target = CallTarget::Direct(candidate);
1100                call.label = CallLabel::new(ocaml_candidate_call_label(target, &arguments));
1101                continue;
1102            }
1103            let Some(name) = (match call.syntax.visible() {
1104                CallSyntax::Path(parts) if parts.len() == 1 => parts.first(),
1105                CallSyntax::Path(_)
1106                | CallSyntax::SelfMethod(_)
1107                | CallSyntax::Method { .. }
1108                | CallSyntax::Expression(_)
1109                | CallSyntax::CompilerConfirmed(_) => None,
1110            }) else {
1111                continue;
1112            };
1113            let candidate =
1114                local_callable_at(name, &function.id, &symbols, &scopes, Some(&call.span))
1115                    .and_then(|id| symbols.get(&id));
1116            let Some(candidate) = candidate else {
1117                continue;
1118            };
1119            call.target = CallTarget::Direct(candidate.id.clone());
1120            if let Some(ordinal) = candidate
1121                .id
1122                .name
1123                .rsplit_once("#closure")
1124                .map(|(_, ordinal)| ordinal)
1125            {
1126                call.label = call.label.with_suffix(&format!(" [closure#{ordinal}]"));
1127            }
1128        }
1129    }
1130}
1131
1132fn local_callable_at(
1133    name: &str,
1134    caller: &SymbolId,
1135    symbols: &OcamlSymbols<'_>,
1136    scopes: &BTreeMap<SymbolId, SourceSpan>,
1137    position: Option<&SourceSpan>,
1138) -> Option<SymbolId> {
1139    let direct_owner = caller.to_string();
1140    let sibling_owner = caller.container.as_deref();
1141    let mut candidates = symbols
1142        .named(name)
1143        .filter(|candidate| {
1144            candidate.id.module == caller.module
1145                && candidate.id.container.is_some()
1146                && (candidate.id.container.as_deref() == Some(direct_owner.as_str())
1147                    || candidate.id.container.as_deref() == sibling_owner
1148                    || candidate.id == *caller)
1149                && (candidate.id == *caller
1150                    || position.is_none_or(|position| {
1151                        scopes
1152                            .get(&candidate.id)
1153                            .is_some_and(|scope| scope.contains(position))
1154                    }))
1155        })
1156        .collect::<Vec<_>>();
1157    candidates.sort_by_key(|candidate| {
1158        let scope = scopes.get(&candidate.id).unwrap_or(&candidate.span);
1159        (
1160            scope.start_line,
1161            scope.start_column,
1162            candidate.span.start_line,
1163            candidate.span.start_column,
1164            candidate.id.clone(),
1165        )
1166    });
1167    candidates.last().map(|candidate| candidate.id.clone())
1168}
1169
1170fn local_callable_source_name(function: &FunctionInfo) -> &str {
1171    ocaml_source_name(&function.id.name)
1172}
1173
1174fn ocaml_source_name(name: &str) -> &str {
1175    let name = name.split_once("#ctx[").map_or(name, |(name, _)| name);
1176    let name = strip_ocaml_numeric_suffix(name, "#closure");
1177    strip_ocaml_numeric_suffix(name, "#binding")
1178}
1179
1180fn strip_ocaml_numeric_suffix<'a>(name: &'a str, marker: &str) -> &'a str {
1181    name.rsplit_once(marker).map_or(name, |(base, suffix)| {
1182        if suffix.chars().all(|character| character.is_ascii_digit()) {
1183            base
1184        } else {
1185            name
1186        }
1187    })
1188}
1189
1190fn redact_anonymous_call_arguments(analysis: &mut FileAnalysis) {
1191    let labels = analysis
1192        .functions
1193        .iter()
1194        .filter(|function| local_callable_source_name(function).starts_with("{lambda:"))
1195        .filter_map(|function| {
1196            let expression = analysis
1197                .facts
1198                .iter()
1199                .find(|fact| fact.subject == function.id && fact.kind == "closure-expression")?;
1200            let lambda = function.label.default.split_whitespace().next()?.to_owned();
1201            Some((expression.value.clone(), lambda))
1202        })
1203        .collect::<Vec<_>>();
1204    for function in &mut analysis.functions {
1205        for call in &mut function.calls {
1206            for (expression, lambda) in &labels {
1207                call.label.default = call.label.default.replace(expression, lambda);
1208                if let Some(typed) = &mut call.label.typed {
1209                    *typed = typed.replace(expression, lambda);
1210                }
1211            }
1212        }
1213    }
1214}
1215
1216#[derive(Clone, Debug, Default, Eq, PartialEq)]
1217struct CallableFlow {
1218    candidates: BTreeSet<SymbolId>,
1219    unresolved_reasons: BTreeSet<UnresolvedReason>,
1220}
1221
1222#[derive(Clone, Debug)]
1223struct OcamlCallFlow {
1224    caller: SymbolId,
1225    target: SymbolId,
1226    arguments: Vec<String>,
1227    span: SourceSpan,
1228}
1229
1230struct OcamlSymbols<'a> {
1231    functions: &'a [FunctionInfo],
1232    by_id: BTreeMap<SymbolId, usize>,
1233    by_name: BTreeMap<String, Vec<usize>>,
1234    by_file: BTreeMap<PathBuf, Vec<usize>>,
1235}
1236
1237impl<'a> OcamlSymbols<'a> {
1238    fn new(functions: &'a [FunctionInfo]) -> Self {
1239        let mut by_id = BTreeMap::new();
1240        let mut by_name = BTreeMap::<String, Vec<usize>>::new();
1241        let mut by_file = BTreeMap::<PathBuf, Vec<usize>>::new();
1242        for (index, function) in functions.iter().enumerate() {
1243            by_id.insert(function.id.clone(), index);
1244            by_name
1245                .entry(local_callable_source_name(function).to_owned())
1246                .or_default()
1247                .push(index);
1248            by_file
1249                .entry(function.span.file.clone())
1250                .or_default()
1251                .push(index);
1252        }
1253        let canonical_files = by_file
1254            .iter()
1255            .filter_map(|(file, functions)| {
1256                let canonical = file.canonicalize().ok()?;
1257                (canonical != *file).then(|| (canonical, functions.clone()))
1258            })
1259            .collect::<Vec<_>>();
1260        for (file, functions) in canonical_files {
1261            by_file.entry(file).or_default().extend(functions);
1262        }
1263        for functions in by_file.values_mut() {
1264            functions.sort_unstable();
1265            functions.dedup();
1266        }
1267        Self {
1268            functions,
1269            by_id,
1270            by_name,
1271            by_file,
1272        }
1273    }
1274
1275    fn get(&self, symbol: &SymbolId) -> Option<&'a FunctionInfo> {
1276        self.by_id
1277            .get(symbol)
1278            .and_then(|index| self.functions.get(*index))
1279    }
1280
1281    fn named(&self, name: &str) -> impl Iterator<Item = &'a FunctionInfo> + '_ {
1282        self.by_name
1283            .get(name)
1284            .into_iter()
1285            .flatten()
1286            .filter_map(|index| self.functions.get(*index))
1287    }
1288
1289    fn resolve(
1290        &self,
1291        parts: &[String],
1292        caller: &SymbolId,
1293        position: Option<&SourceSpan>,
1294    ) -> Option<SymbolId> {
1295        let name = parts.last()?;
1296        let indices = self.by_name.get(name)?;
1297        let mut candidates = indices
1298            .iter()
1299            .filter_map(|index| self.functions.get(*index))
1300            .filter(|candidate| {
1301                if parts.len() == 1 {
1302                    candidate.id.module == caller.module && candidate.id.container.is_none()
1303                } else {
1304                    let requested_module = &parts[..parts.len() - 1];
1305                    candidate.id.container.is_none()
1306                        && requested_module.len() <= candidate.id.module.len()
1307                        && candidate.id.module[candidate.id.module.len() - requested_module.len()..]
1308                            == *requested_module
1309                }
1310            })
1311            .collect::<Vec<_>>();
1312        if candidates.len() == 1 {
1313            return Some(candidates[0].id.clone());
1314        }
1315
1316        if let Some(position) = position {
1317            let same_file = self
1318                .functions_in_file(&position.file)
1319                .filter(|candidate| candidates.iter().any(|item| item.id == candidate.id))
1320                .collect::<Vec<_>>();
1321            if !same_file.is_empty() {
1322                candidates = same_file;
1323                let enclosing_binding = self
1324                    .functions_in_file(&position.file)
1325                    .filter(|function| {
1326                        function.id.container.is_none() && function.span.contains(position)
1327                    })
1328                    .min_by_key(|function| function.span.extent());
1329                let visibility_limit = enclosing_binding
1330                    .map_or((position.start_line, position.start_column), |function| {
1331                        (function.span.start_line, function.span.start_column)
1332                    });
1333                candidates.retain(|candidate| {
1334                    (candidate.span.start_line, candidate.span.start_column) < visibility_limit
1335                });
1336                candidates.sort_by_key(|candidate| {
1337                    (
1338                        candidate.span.start_line,
1339                        candidate.span.start_column,
1340                        candidate.span.end_line,
1341                        candidate.span.end_column,
1342                    )
1343                });
1344                if let Some(candidate) = candidates.last() {
1345                    return Some(candidate.id.clone());
1346                }
1347            }
1348        }
1349
1350        let mut candidates = candidates
1351            .into_iter()
1352            .map(|candidate| candidate.id.clone())
1353            .collect::<Vec<_>>();
1354        candidates.sort();
1355        candidates.dedup();
1356        (candidates.len() == 1).then(|| candidates.remove(0))
1357    }
1358
1359    fn local_target(&self, target: &CallTarget) -> Option<SymbolId> {
1360        let CallTarget::Direct(target) = target else {
1361            return None;
1362        };
1363        if self.by_id.contains_key(target) {
1364            return Some(target.clone());
1365        }
1366        let target_parts = target.qualified_parts();
1367        let mut matches = self
1368            .functions
1369            .iter()
1370            .filter(|function| {
1371                let parts = function.id.qualified_parts();
1372                target_parts.len() <= parts.len()
1373                    && parts[parts.len() - target_parts.len()..] == target_parts
1374            })
1375            .map(|function| function.id.clone())
1376            .collect::<Vec<_>>();
1377        (matches.len() == 1).then(|| matches.remove(0))
1378    }
1379
1380    fn functions_in_file(&self, file: &Path) -> impl Iterator<Item = &'a FunctionInfo> + '_ {
1381        let functions = self.by_file.get(file).or_else(|| {
1382            file.canonicalize()
1383                .ok()
1384                .and_then(|file| self.by_file.get(&file))
1385        });
1386        functions
1387            .into_iter()
1388            .flatten()
1389            .filter_map(|index| self.functions.get(*index))
1390    }
1391}
1392
1393struct OcamlFlowIndex<'a> {
1394    symbols: OcamlSymbols<'a>,
1395    parameters: &'a BTreeMap<SymbolId, Vec<OcamlParameterBinding>>,
1396    return_outcomes: &'a BTreeMap<SymbolId, Vec<String>>,
1397    module_aliases: &'a BTreeMap<Vec<String>, Vec<String>>,
1398    local_values: &'a BTreeMap<(SymbolId, String), Vec<OcamlLocalValue>>,
1399    local_callable_scopes: &'a BTreeMap<SymbolId, SourceSpan>,
1400}
1401
1402#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1403enum OcamlParameterKind {
1404    Value,
1405    Module,
1406}
1407
1408#[derive(Clone, Debug)]
1409struct OcamlParameterBinding {
1410    name: String,
1411    argument_index: usize,
1412    projection: Vec<OcamlProjection>,
1413    kind: OcamlParameterKind,
1414    scope: Option<SourceSpan>,
1415}
1416
1417#[derive(Clone, Debug)]
1418enum OcamlProjection {
1419    Tuple(usize),
1420    Constructor(String),
1421    RecordField(String),
1422}
1423
1424#[derive(Clone, Debug)]
1425struct OcamlLocalValue {
1426    expression: String,
1427    scope: SourceSpan,
1428}
1429
1430fn called_ocaml_parameter_index(
1431    parts: &[String],
1432    bindings: &[OcamlParameterBinding],
1433    position: Option<&SourceSpan>,
1434) -> Option<usize> {
1435    bindings.iter().position(|binding| {
1436        binding
1437            .scope
1438            .as_ref()
1439            .is_none_or(|scope| position.is_some_and(|position| scope.contains(position)))
1440            && match binding.kind {
1441                OcamlParameterKind::Value => parts.len() == 1 && parts[0] == binding.name,
1442                OcamlParameterKind::Module => parts.len() > 1 && parts[0] == binding.name,
1443            }
1444    })
1445}
1446
1447fn ocaml_local_values(
1448    facts: &[LanguageFact],
1449) -> BTreeMap<(SymbolId, String), Vec<OcamlLocalValue>> {
1450    facts.iter().filter(|fact| fact.kind == "local-value").fold(
1451        BTreeMap::new(),
1452        |mut values, fact| {
1453            values
1454                .entry((fact.subject.clone(), fact.key.clone()))
1455                .or_insert_with(Vec::new)
1456                .push(OcamlLocalValue {
1457                    expression: fact.value.clone(),
1458                    scope: fact.span.clone(),
1459                });
1460            values
1461        },
1462    )
1463}
1464
1465fn ocaml_local_callable_scopes(facts: &[LanguageFact]) -> BTreeMap<SymbolId, SourceSpan> {
1466    facts
1467        .iter()
1468        .filter(|fact| fact.kind == "local-callable-scope")
1469        .map(|fact| (fact.subject.clone(), fact.span.clone()))
1470        .collect()
1471}
1472
1473fn unique_ocaml_local_value<'a>(
1474    values: &'a BTreeMap<(SymbolId, String), Vec<OcamlLocalValue>>,
1475    owner: &SymbolId,
1476    name: &str,
1477    position: Option<&SourceSpan>,
1478) -> Option<&'a str> {
1479    scoped_ocaml_local_value(values, owner, name, position)
1480        .map(|candidate| candidate.expression.as_str())
1481}
1482
1483fn scoped_ocaml_local_value<'a>(
1484    values: &'a BTreeMap<(SymbolId, String), Vec<OcamlLocalValue>>,
1485    owner: &SymbolId,
1486    name: &str,
1487    position: Option<&SourceSpan>,
1488) -> Option<&'a OcamlLocalValue> {
1489    let candidates = values.get(&(owner.clone(), name.to_owned()))?;
1490    let mut candidates = candidates
1491        .iter()
1492        .filter(|candidate| position.is_none_or(|position| candidate.scope.contains(position)))
1493        .collect::<Vec<_>>();
1494    candidates.sort_by_key(|candidate| (candidate.scope.start_line, candidate.scope.start_column));
1495    candidates.last().copied()
1496}
1497
1498fn position_before_ocaml_scope(scope: &SourceSpan) -> SourceSpan {
1499    let mut position = scope.clone();
1500    if position.start_column > 0 {
1501        position.start_column -= 1;
1502        position.end_column = position.start_column;
1503    } else if position.start_line > 1 {
1504        position.start_line -= 1;
1505        position.end_line = position.start_line;
1506        position.start_column = usize::MAX;
1507        position.end_column = usize::MAX;
1508    }
1509    position.start_byte = position.start_byte.map(|byte| byte.saturating_sub(1));
1510    position.end_byte = position.start_byte;
1511    position
1512}
1513
1514fn instantiate_ocaml_functors(analysis: &mut FileAnalysis) {
1515    let base_functions = analysis.functions.clone();
1516    let base_facts = analysis.facts.clone();
1517    let functors = base_facts
1518        .iter()
1519        .filter(|fact| fact.kind == "functor-parameter")
1520        .fold(
1521            BTreeMap::<Vec<String>, Vec<String>>::new(),
1522            |mut map, fact| {
1523                let mut module = fact.subject.module.clone();
1524                module.push(fact.subject.name.clone());
1525                map.entry(module).or_default().push(fact.value.clone());
1526                map
1527            },
1528        );
1529    let instances = base_facts
1530        .iter()
1531        .filter(|fact| fact.kind == "functor-instance")
1532        .cloned()
1533        .collect::<Vec<_>>();
1534
1535    for instance in instances {
1536        let mut instance_module = instance.subject.module.clone();
1537        instance_module.push(instance.subject.name.clone());
1538        let raw_functor = instance
1539            .key
1540            .split('.')
1541            .map(ToOwned::to_owned)
1542            .collect::<Vec<_>>();
1543        let mut relative_functor = instance.subject.module.clone();
1544        relative_functor.extend(raw_functor.iter().cloned());
1545        let functor_module = if functors.contains_key(&relative_functor) {
1546            relative_functor
1547        } else {
1548            raw_functor
1549        };
1550        let Some(parameters) = functors.get(&functor_module) else {
1551            continue;
1552        };
1553        let arguments = instance
1554            .value
1555            .split('\t')
1556            .filter(|argument| !argument.is_empty())
1557            .map(|argument| {
1558                let raw = argument
1559                    .split('.')
1560                    .map(ToOwned::to_owned)
1561                    .collect::<Vec<_>>();
1562                let mut relative = instance.subject.module.clone();
1563                relative.extend(raw.iter().cloned());
1564                if base_functions
1565                    .iter()
1566                    .any(|function| function.id.module.starts_with(&relative))
1567                {
1568                    relative
1569                } else {
1570                    raw
1571                }
1572            })
1573            .collect::<Vec<_>>();
1574        if parameters.len() != arguments.len() {
1575            continue;
1576        }
1577        let substitutions = parameters
1578            .iter()
1579            .cloned()
1580            .zip(arguments)
1581            .collect::<Vec<_>>();
1582
1583        let selected = base_functions
1584            .iter()
1585            .filter(|function| function.id.module.starts_with(&functor_module))
1586            .cloned()
1587            .collect::<Vec<_>>();
1588        let ids = selected
1589            .iter()
1590            .map(|function| {
1591                let mut id = function.id.clone();
1592                let suffix = id.module[functor_module.len()..].to_vec();
1593                id.module = instance_module.iter().cloned().chain(suffix).collect();
1594                (function.id.clone(), id)
1595            })
1596            .collect::<BTreeMap<_, _>>();
1597        let containers = ids
1598            .iter()
1599            .map(|(old, new)| (old.to_string(), new.to_string()))
1600            .collect::<BTreeMap<_, _>>();
1601        let mut clones = Vec::new();
1602        for mut function in selected {
1603            let old_id = function.id.clone();
1604            let Some(new_id) = ids.get(&old_id).cloned() else {
1605                continue;
1606            };
1607            function.id = new_id;
1608            if let Some(container) = &function.id.container
1609                && let Some(rewritten) = containers.get(container)
1610            {
1611                function.id.container = Some(rewritten.clone());
1612            }
1613            function.label.default = replace_ocaml_module_prefix(
1614                &function.label.default,
1615                &functor_module,
1616                &instance_module,
1617            );
1618            if let Some(typed) = &mut function.label.typed {
1619                *typed = replace_ocaml_module_prefix(typed, &functor_module, &instance_module);
1620            }
1621            for call in &mut function.calls {
1622                for (parameter, argument_module) in &substitutions {
1623                    rewrite_functor_call(call, parameter, argument_module, &containers, &ids);
1624                }
1625            }
1626            clones.push((old_id, function));
1627        }
1628        for (old_id, function) in &clones {
1629            analysis.facts.extend(
1630                base_facts
1631                    .iter()
1632                    .filter(|fact| fact.subject == *old_id)
1633                    .cloned()
1634                    .map(|mut fact| {
1635                        fact.subject = function.id.clone();
1636                        fact
1637                    }),
1638            );
1639        }
1640        let new_functions = clones
1641            .into_iter()
1642            .map(|(_, function)| function)
1643            .filter(|function| {
1644                !analysis
1645                    .functions
1646                    .iter()
1647                    .any(|existing| existing.id == function.id)
1648            })
1649            .collect::<Vec<_>>();
1650        analysis.functions.extend(new_functions);
1651    }
1652}
1653
1654fn rewrite_functor_call(
1655    call: &mut CallSite,
1656    parameter: &str,
1657    argument_module: &[String],
1658    containers: &BTreeMap<String, String>,
1659    ids: &BTreeMap<SymbolId, SymbolId>,
1660) {
1661    rewrite_functor_syntax(&mut call.syntax, parameter, argument_module);
1662    let parameter_prefix = format!("{parameter}.");
1663    let argument_prefix = format!("{}.", argument_module.join("."));
1664    call.label.default = call
1665        .label
1666        .default
1667        .replacen(&parameter_prefix, &argument_prefix, 1);
1668    if let Some(typed) = &mut call.label.typed {
1669        *typed = typed.replacen(&parameter_prefix, &argument_prefix, 1);
1670    }
1671    match &mut call.target {
1672        CallTarget::Direct(target) => {
1673            if let Some(rewritten) = ids.get(target) {
1674                *target = rewritten.clone();
1675            } else if let Some(container) = &target.container
1676                && let Some(rewritten) = containers.get(container)
1677            {
1678                target.container = Some(rewritten.clone());
1679            }
1680        }
1681        CallTarget::Dynamic {
1682            dispatch,
1683            candidates,
1684            ..
1685        } => {
1686            if let Some(rewritten) = ids.get(dispatch) {
1687                *dispatch = rewritten.clone();
1688            }
1689            for candidate in candidates {
1690                if let Some(rewritten) = ids.get(&candidate.target) {
1691                    candidate.target = rewritten.clone();
1692                }
1693            }
1694        }
1695        CallTarget::Indirect { .. } | CallTarget::Unresolved => {}
1696    }
1697}
1698
1699fn rewrite_functor_syntax(syntax: &mut CallSyntax, parameter: &str, argument_module: &[String]) {
1700    match syntax {
1701        CallSyntax::Path(parts) if parts.first().is_some_and(|part| part == parameter) => {
1702            let mut rewritten = argument_module.to_vec();
1703            rewritten.extend_from_slice(&parts[1..]);
1704            *parts = rewritten;
1705        }
1706        CallSyntax::CompilerConfirmed(syntax) => {
1707            rewrite_functor_syntax(syntax, parameter, argument_module);
1708        }
1709        CallSyntax::Path(_)
1710        | CallSyntax::SelfMethod(_)
1711        | CallSyntax::Method { .. }
1712        | CallSyntax::Expression(_) => {}
1713    }
1714}
1715
1716fn replace_ocaml_module_prefix(label: &str, from: &[String], to: &[String]) -> String {
1717    let from = format!("{}.", from.join("."));
1718    let to = format!("{}.", to.join("."));
1719    label.replacen(&from, &to, 1)
1720}
1721
1722fn resolve_ocaml_function_values(analysis: &mut FileAnalysis) {
1723    instantiate_ocaml_functors(analysis);
1724    let mut functions = analysis.functions.clone();
1725    let parameters = ocaml_parameters(&functions, &analysis.facts);
1726    let return_outcomes = ocaml_return_outcomes(&functions, &analysis.facts);
1727    let module_aliases = ocaml_module_aliases(&functions, &analysis.facts);
1728    let local_values = ocaml_local_values(&analysis.facts);
1729    let local_callable_scopes = ocaml_local_callable_scopes(&analysis.facts);
1730
1731    // Resolve ordinary local/module calls first. Typedtree paths already in
1732    // `CallTarget::Direct` remain authoritative.
1733    let symbol_functions = functions.clone();
1734    let symbols = OcamlSymbols::new(&symbol_functions);
1735    for function in &mut functions {
1736        let parameter_names = parameters.get(&function.id).cloned().unwrap_or_default();
1737        for call in &mut function.calls {
1738            if let CallSyntax::Path(parts) = call.syntax.visible()
1739                && (called_ocaml_parameter_index(parts, &parameter_names, Some(&call.span))
1740                    .is_some()
1741                    || (parts.len() == 1
1742                        && unique_ocaml_local_value(
1743                            &local_values,
1744                            &function.id,
1745                            &parts[0],
1746                            Some(&call.span),
1747                        )
1748                        .is_some()))
1749            {
1750                // Typedtree resolves first-class module members to their
1751                // abstract signature path. The concrete module still comes
1752                // from the caller, so leave this edge for flow specialization.
1753                call.target = CallTarget::Unresolved;
1754                continue;
1755            }
1756            if let CallTarget::Direct(target) = &call.target {
1757                let parts = target
1758                    .qualified_parts()
1759                    .into_iter()
1760                    .map(ToOwned::to_owned)
1761                    .collect::<Vec<_>>();
1762                if let Some(expanded) = expand_ocaml_module_alias(&parts, &module_aliases)
1763                    && let Some(resolved) =
1764                        symbols.resolve(&expanded, &function.id, Some(&call.span))
1765                {
1766                    call.target = CallTarget::Direct(resolved);
1767                    continue;
1768                }
1769            }
1770            if !matches!(call.target, CallTarget::Unresolved) {
1771                continue;
1772            }
1773            let CallSyntax::Path(parts) = call.syntax.visible() else {
1774                continue;
1775            };
1776            let expanded =
1777                expand_ocaml_module_alias(parts, &module_aliases).unwrap_or_else(|| parts.to_vec());
1778            if let Some(target) = symbols.resolve(&expanded, &function.id, Some(&call.span)) {
1779                call.target = CallTarget::Direct(target);
1780            }
1781        }
1782    }
1783
1784    let mut called_parameters = functions
1785        .iter()
1786        .flat_map(|function| {
1787            let names = parameters.get(&function.id).cloned().unwrap_or_default();
1788            function.calls.iter().filter_map(move |call| {
1789                let CallSyntax::Path(parts) = call.syntax.visible() else {
1790                    return None;
1791                };
1792                called_ocaml_parameter_index(parts, &names, Some(&call.span))
1793                    .map(|index| (function.id.clone(), index))
1794            })
1795        })
1796        .collect::<BTreeSet<_>>();
1797    let symbols = OcamlSymbols::new(&functions);
1798    let return_called = ocaml_return_called_functions(&symbols);
1799    for (function, outcomes) in &return_outcomes {
1800        if !return_called.contains(function) {
1801            continue;
1802        }
1803        let names = parameters.get(function).cloned().unwrap_or_default();
1804        for outcome in outcomes {
1805            if let Some(index) = names.iter().position(|binding| binding.name == *outcome) {
1806                called_parameters.insert((function.clone(), index));
1807            }
1808        }
1809    }
1810
1811    let mut call_flows = Vec::new();
1812    for function in &functions {
1813        for call in &function.calls {
1814            let Some(target) = symbols.local_target(&call.target) else {
1815                continue;
1816            };
1817            call_flows.push(OcamlCallFlow {
1818                caller: function.id.clone(),
1819                target,
1820                arguments: ocaml_call_arguments(call),
1821                span: call.span.clone(),
1822            });
1823        }
1824    }
1825    loop {
1826        let previous_len = called_parameters.len();
1827        for call in &call_flows {
1828            let caller_parameters = parameters.get(&call.caller).cloned().unwrap_or_default();
1829            let target_parameters = parameters.get(&call.target).cloned().unwrap_or_default();
1830            for (target_parameter, binding) in target_parameters.iter().enumerate() {
1831                if !called_parameters.contains(&(call.target.clone(), target_parameter)) {
1832                    continue;
1833                }
1834                let Some(argument) = call
1835                    .arguments
1836                    .get(binding.argument_index)
1837                    .and_then(|argument| project_ocaml_argument(argument, &binding.projection))
1838                else {
1839                    continue;
1840                };
1841                let outcomes = ocaml_callable_outcome_names(&argument);
1842                let module_argument = ocaml_module_package_path(&argument);
1843                for (caller_parameter, binding) in caller_parameters.iter().enumerate() {
1844                    let passes_module_parameter = binding.kind == OcamlParameterKind::Module
1845                        && module_argument
1846                            .as_ref()
1847                            .is_some_and(|parts| parts.len() == 1 && parts[0] == binding.name);
1848                    if outcomes.contains(&binding.name) || passes_module_parameter {
1849                        called_parameters.insert((call.caller.clone(), caller_parameter));
1850                    }
1851                }
1852            }
1853        }
1854        if called_parameters.len() == previous_len {
1855            break;
1856        }
1857    }
1858    let flow_index = OcamlFlowIndex {
1859        symbols,
1860        parameters: &parameters,
1861        return_outcomes: &return_outcomes,
1862        module_aliases: &module_aliases,
1863        local_values: &local_values,
1864        local_callable_scopes: &local_callable_scopes,
1865    };
1866    let (functions, facts) = specialize_ocaml_function_values(
1867        &functions,
1868        &analysis.facts,
1869        &called_parameters,
1870        &call_flows,
1871        &flow_index,
1872    );
1873    analysis.functions = functions;
1874    analysis.facts = facts;
1875}
1876
1877fn ocaml_module_aliases(
1878    functions: &[FunctionInfo],
1879    facts: &[LanguageFact],
1880) -> BTreeMap<Vec<String>, Vec<String>> {
1881    facts
1882        .iter()
1883        .filter(|fact| fact.kind == "module-alias")
1884        .map(|fact| {
1885            let mut alias = fact.subject.module.clone();
1886            alias.push(fact.subject.name.clone());
1887            let target = fact
1888                .value
1889                .split('.')
1890                .map(ToOwned::to_owned)
1891                .collect::<Vec<_>>();
1892            let mut relative = fact.subject.module.clone();
1893            relative.extend(target.iter().cloned());
1894            let target = if functions
1895                .iter()
1896                .any(|function| function.id.module.starts_with(&relative))
1897            {
1898                relative
1899            } else {
1900                target
1901            };
1902            (alias, target)
1903        })
1904        .collect()
1905}
1906
1907fn expand_ocaml_module_alias(
1908    parts: &[String],
1909    aliases: &BTreeMap<Vec<String>, Vec<String>>,
1910) -> Option<Vec<String>> {
1911    let mut expanded = parts.to_vec();
1912    let mut changed = false;
1913    let mut seen = BTreeSet::new();
1914    while let Some((alias, target)) = aliases
1915        .iter()
1916        .filter(|(alias, _)| expanded.starts_with(alias))
1917        .max_by_key(|(alias, _)| alias.len())
1918    {
1919        if !seen.insert(alias.clone()) {
1920            break;
1921        }
1922        let mut next = target.clone();
1923        next.extend_from_slice(&expanded[alias.len()..]);
1924        expanded = next;
1925        changed = true;
1926    }
1927    changed.then_some(expanded)
1928}
1929
1930fn ocaml_return_called_functions(symbols: &OcamlSymbols<'_>) -> BTreeSet<SymbolId> {
1931    symbols
1932        .functions
1933        .iter()
1934        .flat_map(|caller| {
1935            caller.calls.iter().filter_map(|call| {
1936                let CallSyntax::Expression(expression) = call.syntax.visible() else {
1937                    return None;
1938                };
1939                let parts = ocaml_applied_function_path(expression)?;
1940                symbols.resolve(&parts, &caller.id, Some(&call.span))
1941            })
1942        })
1943        .collect()
1944}
1945
1946fn ocaml_applied_function_path(expression: &str) -> Option<Vec<String>> {
1947    let tree = parse_ocaml_expression(expression)?;
1948    if tree.root_node().has_error() {
1949        return None;
1950    }
1951    let mut node = tree.root_node().named_child(0)?;
1952    while matches!(
1953        node.kind(),
1954        "expression_item"
1955            | "parenthesized_expression"
1956            | "typed_expression"
1957            | "coercion_expression"
1958            | "local_open_expression"
1959    ) {
1960        node = node
1961            .child_by_field_name("body")
1962            .or_else(|| node.child_by_field_name("expression"))
1963            .or_else(|| node.named_child(0))?;
1964    }
1965    (node.kind() == "application_expression")
1966        .then(|| node.child_by_field_name("function"))
1967        .flatten()
1968        .and_then(|function| ocaml_value_path(function, expression))
1969}
1970
1971fn specialize_ocaml_function_values(
1972    functions: &[FunctionInfo],
1973    facts: &[LanguageFact],
1974    called_parameters: &BTreeSet<(SymbolId, usize)>,
1975    call_flows: &[OcamlCallFlow],
1976    flow_index: &OcamlFlowIndex<'_>,
1977) -> (Vec<FunctionInfo>, Vec<LanguageFact>) {
1978    let parameters = flow_index.parameters;
1979    let index = functions
1980        .iter()
1981        .enumerate()
1982        .map(|(index, function)| (function.id.clone(), index))
1983        .collect::<BTreeMap<_, _>>();
1984    let mut incoming = vec![0usize; functions.len()];
1985    for call in call_flows {
1986        if let Some(target) = index.get(&call.target) {
1987            incoming[*target] += 1;
1988        }
1989        for argument in &call.arguments {
1990            if let Some(candidate) = argument_callable_symbol(
1991                argument,
1992                &call.caller,
1993                &flow_index.symbols,
1994                Some(&call.span),
1995            ) && let Some(target) = index.get(&candidate)
1996            {
1997                incoming[*target] += 1;
1998            }
1999        }
2000    }
2001
2002    let mut pending = VecDeque::<(usize, Vec<CallableFlow>)>::new();
2003    for (function, incoming) in incoming.iter().enumerate() {
2004        if *incoming == 0 {
2005            pending.push_back((
2006                function,
2007                root_ocaml_context(&functions[function], parameters, called_parameters),
2008            ));
2009        }
2010    }
2011    let mut visited = BTreeSet::new();
2012    let mut covered = BTreeSet::new();
2013    let mut specialized = Vec::new();
2014    let mut specialized_facts = Vec::new();
2015
2016    loop {
2017        while let Some((raw_index, context)) = pending.pop_front() {
2018            let raw = &functions[raw_index];
2019            let id = specialized_ocaml_id(raw, &context, called_parameters);
2020            // Distinct source bindings can intentionally shadow to the same
2021            // OCaml path. Even if their display identity currently collapses,
2022            // each raw node must be consumed or the disconnected-component
2023            // fallback below will enqueue it forever.
2024            covered.insert(raw_index);
2025            if !visited.insert(id.clone()) {
2026                continue;
2027            }
2028            let names = parameters.get(&raw.id).cloned().unwrap_or_default();
2029            let mut function = raw.clone();
2030            function.id = id.clone();
2031
2032            for call in &mut function.calls {
2033                if let CallSyntax::Expression(expression) = call.syntax.visible()
2034                    && !matches!(
2035                        call.target,
2036                        CallTarget::Direct(_) | CallTarget::Dynamic { .. }
2037                    )
2038                {
2039                    let flow = argument_callable_flow(
2040                        expression,
2041                        raw,
2042                        &context,
2043                        flow_index,
2044                        Some(&call.span),
2045                    );
2046                    let arguments = ocaml_call_arguments(call);
2047                    let candidates = flow
2048                        .candidates
2049                        .iter()
2050                        .filter_map(|target| {
2051                            let target_index = *index.get(target)?;
2052                            let target_function = &functions[target_index];
2053                            let target_context = ocaml_call_context(
2054                                target_function,
2055                                &arguments,
2056                                &call.span,
2057                                raw,
2058                                &context,
2059                                flow_index,
2060                                called_parameters,
2061                            );
2062                            let target = specialized_ocaml_id(
2063                                target_function,
2064                                &target_context,
2065                                called_parameters,
2066                            );
2067                            pending.push_back((target_index, target_context));
2068                            Some(DispatchCandidate {
2069                                target,
2070                                label: CallLabel::new(ocaml_candidate_call_label(
2071                                    target_function,
2072                                    &arguments,
2073                                )),
2074                            })
2075                        })
2076                        .collect::<Vec<_>>();
2077                    call.target = CallTarget::dynamic(
2078                        SymbolId {
2079                            language: LanguageId::new("ocaml"),
2080                            module: raw.id.module.clone(),
2081                            container: Some(raw.id.name.clone()),
2082                            name: expression.clone(),
2083                        },
2084                        candidates,
2085                        DispatchEvidence::ExactFlow,
2086                        flow.unresolved_reasons,
2087                    );
2088                    continue;
2089                }
2090
2091                let CallSyntax::Path(parts) = call.syntax.visible() else {
2092                    continue;
2093                };
2094                let local_expression = (parts.len() == 1)
2095                    .then(|| {
2096                        unique_ocaml_local_value(
2097                            flow_index.local_values,
2098                            &raw.id,
2099                            &parts[0],
2100                            Some(&call.span),
2101                        )
2102                    })
2103                    .flatten();
2104                let flow_and_binding =
2105                    local_expression
2106                        .map(|expression| {
2107                            (
2108                                argument_callable_flow(
2109                                    expression,
2110                                    raw,
2111                                    &context,
2112                                    flow_index,
2113                                    Some(&call.span),
2114                                ),
2115                                None,
2116                            )
2117                        })
2118                        .or_else(|| {
2119                            let parameter_index =
2120                                called_ocaml_parameter_index(parts, &names, Some(&call.span))?;
2121                            let binding = &names[parameter_index];
2122                            let flow = context.get(parameter_index).cloned().unwrap_or_else(|| {
2123                                CallableFlow {
2124                                    candidates: BTreeSet::new(),
2125                                    unresolved_reasons: [UnresolvedReason::AnalysisLimit]
2126                                        .into_iter()
2127                                        .collect(),
2128                                }
2129                            });
2130                            Some((flow, Some(binding)))
2131                        });
2132                if let Some((flow, binding)) = flow_and_binding {
2133                    let arguments = ocaml_call_arguments(call);
2134                    let candidates = flow
2135                        .candidates
2136                        .iter()
2137                        .filter_map(|target| {
2138                            let target = match binding.map(|binding| binding.kind) {
2139                                None | Some(OcamlParameterKind::Value) => target.clone(),
2140                                Some(OcamlParameterKind::Module) => {
2141                                    let mut target_parts = ocaml_module_flow_path(target)?.to_vec();
2142                                    target_parts.extend_from_slice(&parts[1..]);
2143                                    flow_index.symbols.resolve(
2144                                        &target_parts,
2145                                        &raw.id,
2146                                        Some(&call.span),
2147                                    )?
2148                                }
2149                            };
2150                            let target_index = *index.get(&target)?;
2151                            let target_function = &functions[target_index];
2152                            let target_context = ocaml_call_context(
2153                                target_function,
2154                                &arguments,
2155                                &call.span,
2156                                raw,
2157                                &context,
2158                                flow_index,
2159                                called_parameters,
2160                            );
2161                            let specialized_target = specialized_ocaml_id(
2162                                target_function,
2163                                &target_context,
2164                                called_parameters,
2165                            );
2166                            pending.push_back((target_index, target_context));
2167                            Some(DispatchCandidate {
2168                                target: specialized_target,
2169                                label: CallLabel::new(ocaml_candidate_call_label(
2170                                    target_function,
2171                                    &arguments,
2172                                )),
2173                            })
2174                        })
2175                        .collect::<Vec<_>>();
2176                    call.target = CallTarget::dynamic(
2177                        SymbolId {
2178                            language: LanguageId::new("ocaml"),
2179                            module: raw.id.module.clone(),
2180                            container: Some(raw.id.name.clone()),
2181                            name: parts.join("."),
2182                        },
2183                        candidates,
2184                        DispatchEvidence::ExactFlow,
2185                        flow.unresolved_reasons,
2186                    );
2187                    continue;
2188                }
2189
2190                let Some(target) = flow_index.symbols.local_target(&call.target) else {
2191                    continue;
2192                };
2193                let Some(target_index) = index.get(&target).copied() else {
2194                    continue;
2195                };
2196                let target_function = &functions[target_index];
2197                let arguments = ocaml_call_arguments(call);
2198                let target_context = ocaml_call_context(
2199                    target_function,
2200                    &arguments,
2201                    &call.span,
2202                    raw,
2203                    &context,
2204                    flow_index,
2205                    called_parameters,
2206                );
2207                let target =
2208                    specialized_ocaml_id(target_function, &target_context, called_parameters);
2209                pending.push_back((target_index, target_context));
2210                call.target = CallTarget::Direct(target);
2211            }
2212
2213            specialized_facts.extend(
2214                facts
2215                    .iter()
2216                    .filter(|fact| fact.subject == raw.id)
2217                    .cloned()
2218                    .map(|mut fact| {
2219                        fact.subject = id.clone();
2220                        fact
2221                    }),
2222            );
2223            specialized.push(function);
2224        }
2225
2226        let Some(uncovered) = (0..functions.len()).find(|index| !covered.contains(index)) else {
2227            break;
2228        };
2229        pending.push_back((
2230            uncovered,
2231            root_ocaml_context(&functions[uncovered], parameters, called_parameters),
2232        ));
2233    }
2234    (specialized, specialized_facts)
2235}
2236
2237fn root_ocaml_context(
2238    function: &FunctionInfo,
2239    parameters: &BTreeMap<SymbolId, Vec<OcamlParameterBinding>>,
2240    called_parameters: &BTreeSet<(SymbolId, usize)>,
2241) -> Vec<CallableFlow> {
2242    parameters
2243        .get(&function.id)
2244        .into_iter()
2245        .flatten()
2246        .enumerate()
2247        .map(|(index, _)| CallableFlow {
2248            candidates: BTreeSet::new(),
2249            unresolved_reasons: called_parameters
2250                .contains(&(function.id.clone(), index))
2251                .then_some(UnresolvedReason::OpaqueInput)
2252                .into_iter()
2253                .collect(),
2254        })
2255        .collect()
2256}
2257
2258#[allow(clippy::too_many_arguments)]
2259fn ocaml_call_context(
2260    target: &FunctionInfo,
2261    arguments: &[String],
2262    call_span: &SourceSpan,
2263    caller: &FunctionInfo,
2264    caller_context: &[CallableFlow],
2265    flow_index: &OcamlFlowIndex<'_>,
2266    called_parameters: &BTreeSet<(SymbolId, usize)>,
2267) -> Vec<CallableFlow> {
2268    flow_index
2269        .parameters
2270        .get(&target.id)
2271        .into_iter()
2272        .flatten()
2273        .enumerate()
2274        .map(|(index, binding)| {
2275            if !called_parameters.contains(&(target.id.clone(), index)) {
2276                return CallableFlow::default();
2277            }
2278            arguments
2279                .get(binding.argument_index)
2280                .and_then(|argument| project_ocaml_argument(argument, &binding.projection))
2281                .map_or_else(
2282                    || CallableFlow {
2283                        candidates: BTreeSet::new(),
2284                        unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
2285                    },
2286                    |argument| {
2287                        argument_callable_flow(
2288                            &argument,
2289                            caller,
2290                            caller_context,
2291                            flow_index,
2292                            Some(call_span),
2293                        )
2294                    },
2295                )
2296        })
2297        .collect()
2298}
2299
2300fn specialized_ocaml_id(
2301    function: &FunctionInfo,
2302    context: &[CallableFlow],
2303    called_parameters: &BTreeSet<(SymbolId, usize)>,
2304) -> SymbolId {
2305    let bindings = context
2306        .iter()
2307        .enumerate()
2308        .filter(|(index, _)| called_parameters.contains(&(function.id.clone(), *index)))
2309        .map(|(index, flow)| {
2310            let mut values = flow
2311                .candidates
2312                .iter()
2313                .map(ToString::to_string)
2314                .collect::<Vec<_>>();
2315            for reason in &flow.unresolved_reasons {
2316                values.push(format!("?{reason}"));
2317            }
2318            format!("{index}={}", values.join("|"))
2319        })
2320        .collect::<Vec<_>>();
2321    if bindings.is_empty() {
2322        return function.id.clone();
2323    }
2324    let mut id = function.id.clone();
2325    id.name = format!("{}#ctx[{}]", id.name, bindings.join(","));
2326    id
2327}
2328
2329fn ocaml_parameters(
2330    functions: &[FunctionInfo],
2331    facts: &[LanguageFact],
2332) -> BTreeMap<SymbolId, Vec<OcamlParameterBinding>> {
2333    functions
2334        .iter()
2335        .map(|function| {
2336            let mut parameters = Vec::new();
2337            for (argument_index, fact) in facts
2338                .iter()
2339                .filter(|fact| fact.subject == function.id && fact.kind == "parameter")
2340                .enumerate()
2341            {
2342                collect_ocaml_parameter_bindings(
2343                    &fact.key,
2344                    argument_index,
2345                    &mut Vec::new(),
2346                    &mut parameters,
2347                );
2348            }
2349            for fact in facts
2350                .iter()
2351                .filter(|fact| fact.subject == function.id && fact.kind == "parameter-refinement")
2352            {
2353                let Some((source, pattern)) = fact.value.split_once('\n') else {
2354                    continue;
2355                };
2356                let Some(base) = parameters
2357                    .iter()
2358                    .find(|binding| binding.scope.is_none() && binding.name == source)
2359                    .cloned()
2360                else {
2361                    continue;
2362                };
2363                let mut refined = Vec::new();
2364                let mut projection = base.projection.clone();
2365                collect_ocaml_parameter_bindings(
2366                    pattern,
2367                    base.argument_index,
2368                    &mut projection,
2369                    &mut refined,
2370                );
2371                for mut binding in refined {
2372                    if binding.name != fact.key {
2373                        continue;
2374                    }
2375                    binding.scope = Some(fact.span.clone());
2376                    parameters.push(binding);
2377                }
2378            }
2379            (function.id.clone(), parameters)
2380        })
2381        .collect()
2382}
2383
2384fn ocaml_return_outcomes(
2385    functions: &[FunctionInfo],
2386    facts: &[LanguageFact],
2387) -> BTreeMap<SymbolId, Vec<String>> {
2388    functions
2389        .iter()
2390        .map(|function| {
2391            let outcomes = facts
2392                .iter()
2393                .filter(|fact| fact.subject == function.id && fact.kind == "callable-return")
2394                .map(|fact| fact.value.clone())
2395                .collect();
2396            (function.id.clone(), outcomes)
2397        })
2398        .collect()
2399}
2400
2401fn collect_ocaml_parameter_bindings(
2402    pattern: &str,
2403    argument_index: usize,
2404    projection: &mut Vec<OcamlProjection>,
2405    bindings: &mut Vec<OcamlParameterBinding>,
2406) {
2407    let unwrapped = strip_outer_ocaml_parentheses(pattern).trim();
2408    if let Some(module) = unwrapped.strip_prefix("module ") {
2409        let name = module.split([' ', ':']).next().unwrap_or(module).trim();
2410        if !name.is_empty() {
2411            bindings.push(OcamlParameterBinding {
2412                name: name.to_owned(),
2413                argument_index,
2414                projection: projection.clone(),
2415                kind: OcamlParameterKind::Module,
2416                scope: None,
2417            });
2418        }
2419        return;
2420    }
2421    let pattern = normalize_ocaml_pattern(pattern);
2422    let tuple = split_top_level_ocaml(&pattern, ',');
2423    if tuple.len() > 1 {
2424        for (index, element) in tuple.into_iter().enumerate() {
2425            projection.push(OcamlProjection::Tuple(index));
2426            collect_ocaml_parameter_bindings(element, argument_index, projection, bindings);
2427            projection.pop();
2428        }
2429        return;
2430    }
2431    if let Some(fields) = split_ocaml_record_fields(&pattern) {
2432        for (field, value) in fields {
2433            projection.push(OcamlProjection::RecordField(field));
2434            collect_ocaml_parameter_bindings(&value, argument_index, projection, bindings);
2435            projection.pop();
2436        }
2437        return;
2438    }
2439    if let Some((constructor, payload)) = split_ocaml_constructor_application(&pattern) {
2440        projection.push(OcamlProjection::Constructor(constructor.to_owned()));
2441        collect_ocaml_parameter_bindings(payload, argument_index, projection, bindings);
2442        projection.pop();
2443        return;
2444    }
2445    if is_value_identifier(&pattern) && pattern != "_" {
2446        bindings.push(OcamlParameterBinding {
2447            name: pattern,
2448            argument_index,
2449            projection: projection.clone(),
2450            kind: OcamlParameterKind::Value,
2451            scope: None,
2452        });
2453    }
2454}
2455
2456fn normalize_ocaml_pattern(pattern: &str) -> String {
2457    let mut pattern = pattern.trim();
2458    if pattern.starts_with(['~', '?']) {
2459        let stripped = &pattern[1..];
2460        pattern = stripped.split_once(':').map_or_else(
2461            || stripped.split('=').next().unwrap_or(stripped),
2462            |(_, value)| value,
2463        );
2464    }
2465    strip_outer_ocaml_parentheses(pattern).trim().to_owned()
2466}
2467
2468fn project_ocaml_argument(argument: &str, projection: &[OcamlProjection]) -> Option<String> {
2469    let mut value = argument.trim().to_owned();
2470    if value.starts_with(['~', '?']) {
2471        let stripped = &value[1..];
2472        value = stripped
2473            .split_once(':')
2474            .map_or(stripped, |(_, value)| value)
2475            .to_owned();
2476    }
2477    for step in projection {
2478        let current = strip_outer_ocaml_parentheses(&value).trim();
2479        match step {
2480            OcamlProjection::Tuple(index) => {
2481                let elements = split_top_level_ocaml(current, ',');
2482                value = elements.get(*index)?.trim().to_owned();
2483            }
2484            OcamlProjection::Constructor(expected) => {
2485                let (constructor, payload) = split_ocaml_constructor_application(current)?;
2486                if constructor != expected {
2487                    return None;
2488                }
2489                value = payload.to_owned();
2490            }
2491            OcamlProjection::RecordField(expected) => {
2492                let fields = split_ocaml_record_fields(current)?;
2493                value = fields
2494                    .into_iter()
2495                    .find(|(field, _)| field == expected)
2496                    .map(|(_, value)| value)?;
2497            }
2498        }
2499    }
2500    Some(normalize_source(
2501        strip_outer_ocaml_parentheses(&value).trim(),
2502    ))
2503}
2504
2505fn split_ocaml_record_fields(value: &str) -> Option<Vec<(String, String)>> {
2506    let value = strip_outer_ocaml_parentheses(value).trim();
2507    let inner = value.strip_prefix('{')?.strip_suffix('}')?.trim();
2508    if inner.is_empty() || split_top_level_ocaml(inner, ';').contains(&"_") {
2509        return None;
2510    }
2511    let mut fields = Vec::new();
2512    for field in split_top_level_ocaml(inner, ';') {
2513        let field = field.trim();
2514        if field.is_empty() {
2515            continue;
2516        }
2517        let assignment = split_top_level_ocaml(field, '=');
2518        let (path, value) = match assignment.as_slice() {
2519            [path] => (*path, *path),
2520            [path, value] => (*path, *value),
2521            _ => return None,
2522        };
2523        let name = path.rsplit('.').next()?.trim();
2524        if !is_value_identifier(name) {
2525            return None;
2526        }
2527        fields.push((name.to_owned(), normalize_source(value)));
2528    }
2529    (!fields.is_empty()).then_some(fields)
2530}
2531
2532fn split_ocaml_constructor_application(value: &str) -> Option<(&str, &str)> {
2533    let value = strip_outer_ocaml_parentheses(value).trim();
2534    let mut depth = 0usize;
2535    let mut split = None;
2536    for (index, character) in value.char_indices() {
2537        match character {
2538            '(' | '[' | '{' => depth += 1,
2539            ')' | ']' | '}' => depth = depth.saturating_sub(1),
2540            character if character.is_whitespace() && depth == 0 => {
2541                split = Some(index);
2542                break;
2543            }
2544            _ => {}
2545        }
2546    }
2547    let split = split?;
2548    let constructor = value[..split].trim();
2549    let payload = value[split..].trim();
2550    let leaf = constructor.rsplit('.').next().unwrap_or(constructor);
2551    ((leaf.starts_with('`')
2552        || leaf
2553            .chars()
2554            .next()
2555            .is_some_and(|character| character.is_ascii_uppercase()))
2556        && !payload.is_empty())
2557    .then_some((constructor, payload))
2558}
2559
2560fn strip_outer_ocaml_parentheses(mut value: &str) -> &str {
2561    loop {
2562        value = value.trim();
2563        let Some(inner) = value.strip_prefix('(') else {
2564            return value;
2565        };
2566        let mut depth = 1usize;
2567        let mut in_string = false;
2568        let mut escaped = false;
2569        let mut closing = None;
2570        for (index, character) in inner.char_indices() {
2571            if in_string {
2572                if escaped {
2573                    escaped = false;
2574                } else if character == '\\' {
2575                    escaped = true;
2576                } else if character == '"' {
2577                    in_string = false;
2578                }
2579                continue;
2580            }
2581            match character {
2582                '"' => in_string = true,
2583                '(' => depth += 1,
2584                ')' => {
2585                    depth = depth.saturating_sub(1);
2586                    if depth == 0 {
2587                        closing = Some(index);
2588                        break;
2589                    }
2590                }
2591                _ => {}
2592            }
2593        }
2594        let Some(closing) = closing else {
2595            return value;
2596        };
2597        if closing + ')'.len_utf8() != inner.len() {
2598            return value;
2599        }
2600        value = &inner[..closing];
2601    }
2602}
2603
2604fn split_top_level_ocaml(value: &str, separator: char) -> Vec<&str> {
2605    let mut result = Vec::new();
2606    let mut start = 0usize;
2607    let mut depth = 0usize;
2608    let mut in_string = false;
2609    let mut escaped = false;
2610    for (index, character) in value.char_indices() {
2611        if in_string {
2612            if escaped {
2613                escaped = false;
2614            } else if character == '\\' {
2615                escaped = true;
2616            } else if character == '"' {
2617                in_string = false;
2618            }
2619            continue;
2620        }
2621        match character {
2622            '"' => in_string = true,
2623            '(' | '[' | '{' => depth += 1,
2624            ')' | ']' | '}' => depth = depth.saturating_sub(1),
2625            _ if character == separator && depth == 0 => {
2626                result.push(value[start..index].trim());
2627                start = index + character.len_utf8();
2628            }
2629            _ => {}
2630        }
2631    }
2632    result.push(value[start..].trim());
2633    result
2634}
2635
2636fn argument_callable_flow(
2637    argument: &str,
2638    caller: &FunctionInfo,
2639    caller_context: &[CallableFlow],
2640    flow_index: &OcamlFlowIndex<'_>,
2641    position: Option<&SourceSpan>,
2642) -> CallableFlow {
2643    if let Some(mut module) = ocaml_module_package_path(argument) {
2644        if module.len() == 1
2645            && let Some(index) = flow_index
2646                .parameters
2647                .get(&caller.id)
2648                .and_then(|parameters| {
2649                    parameters.iter().position(|parameter| {
2650                        parameter.kind == OcamlParameterKind::Module && parameter.name == module[0]
2651                    })
2652                })
2653        {
2654            return caller_context
2655                .get(index)
2656                .cloned()
2657                .unwrap_or_else(opaque_callable_flow);
2658        }
2659        if let Some(expanded) = expand_ocaml_module_alias(&module, flow_index.module_aliases) {
2660            module = expanded;
2661        }
2662        return CallableFlow {
2663            candidates: BTreeSet::from([ocaml_module_flow_candidate(module)]),
2664            unresolved_reasons: BTreeSet::new(),
2665        };
2666    }
2667    let argument = normalize_ocaml_callable_argument(argument);
2668    if let Some(flow) =
2669        parse_ocaml_callable_flow(argument, caller, caller_context, flow_index, position)
2670    {
2671        return flow;
2672    }
2673    if let Some(candidate) =
2674        argument_callable_symbol(argument, &caller.id, &flow_index.symbols, position)
2675    {
2676        return CallableFlow {
2677            candidates: BTreeSet::from([candidate]),
2678            unresolved_reasons: BTreeSet::new(),
2679        };
2680    }
2681    CallableFlow {
2682        candidates: BTreeSet::new(),
2683        unresolved_reasons: [UnresolvedReason::UnsupportedConstruct]
2684            .into_iter()
2685            .collect(),
2686    }
2687}
2688
2689const OCAML_MODULE_FLOW_MARKER: &str = "{module}";
2690
2691fn ocaml_module_package_path(argument: &str) -> Option<Vec<String>> {
2692    let argument = strip_outer_ocaml_parentheses(argument).trim();
2693    let module = argument.strip_prefix("module ")?.trim();
2694    let module = split_top_level_ocaml(module, ':')
2695        .into_iter()
2696        .next()?
2697        .trim();
2698    let parts = module
2699        .split('.')
2700        .map(str::trim)
2701        .filter(|part| !part.is_empty() && part.chars().all(is_ocaml_path_character))
2702        .map(ToOwned::to_owned)
2703        .collect::<Vec<_>>();
2704    (!parts.is_empty()).then_some(parts)
2705}
2706
2707fn is_ocaml_path_character(character: char) -> bool {
2708    character == '_' || character == '\'' || character.is_alphanumeric()
2709}
2710
2711fn ocaml_module_flow_candidate(module: Vec<String>) -> SymbolId {
2712    SymbolId {
2713        language: LanguageId::new("ocaml"),
2714        module,
2715        container: None,
2716        name: OCAML_MODULE_FLOW_MARKER.to_owned(),
2717    }
2718}
2719
2720fn ocaml_module_flow_path(candidate: &SymbolId) -> Option<&[String]> {
2721    (candidate.language.0 == "ocaml"
2722        && candidate.container.is_none()
2723        && candidate.name == OCAML_MODULE_FLOW_MARKER)
2724        .then_some(candidate.module.as_slice())
2725}
2726
2727fn parse_ocaml_callable_flow(
2728    expression: &str,
2729    caller: &FunctionInfo,
2730    caller_context: &[CallableFlow],
2731    flow_index: &OcamlFlowIndex<'_>,
2732    position: Option<&SourceSpan>,
2733) -> Option<CallableFlow> {
2734    let tree = parse_ocaml_expression(expression)?;
2735    if tree.root_node().has_error() {
2736        return None;
2737    }
2738    let node = tree.root_node().named_child(0)?;
2739    ocaml_callable_node_flow(
2740        node,
2741        expression,
2742        caller,
2743        caller_context,
2744        flow_index,
2745        position,
2746    )
2747}
2748
2749fn ocaml_callable_outcome_names(expression: &str) -> BTreeSet<String> {
2750    let expression = normalize_ocaml_callable_argument(expression);
2751    let Some(tree) = parse_ocaml_expression(expression) else {
2752        return BTreeSet::new();
2753    };
2754    if tree.root_node().has_error() {
2755        return BTreeSet::new();
2756    }
2757    let Some(node) = tree.root_node().named_child(0) else {
2758        return BTreeSet::new();
2759    };
2760    let mut names = BTreeSet::new();
2761    collect_ocaml_callable_outcomes(node, expression, &mut names);
2762    names
2763}
2764
2765fn collect_ocaml_callable_outcomes(node: Node<'_>, source: &str, names: &mut BTreeSet<String>) {
2766    if matches!(
2767        node.kind(),
2768        "value_path" | "value_name" | "parenthesized_operator"
2769    ) {
2770        names.insert(normalize_source(node_text(node, source)));
2771        return;
2772    }
2773    if matches!(
2774        node.kind(),
2775        "expression_item"
2776            | "parenthesized_expression"
2777            | "typed_expression"
2778            | "coercion_expression"
2779            | "let_expression"
2780            | "local_open_expression"
2781    ) {
2782        if let Some(expression) = node
2783            .child_by_field_name("body")
2784            .or_else(|| node.child_by_field_name("expression"))
2785            .or_else(|| node.named_child(0))
2786        {
2787            collect_ocaml_callable_outcomes(expression, source, names);
2788        }
2789        return;
2790    }
2791    if node.kind() == "sequence_expression" {
2792        let mut cursor = node.walk();
2793        if let Some(last) = node.named_children(&mut cursor).last() {
2794            collect_ocaml_callable_outcomes(last, source, names);
2795        }
2796        return;
2797    }
2798    if node.kind() == "if_expression" {
2799        let mut cursor = node.walk();
2800        for clause in node
2801            .named_children(&mut cursor)
2802            .filter(|child| matches!(child.kind(), "then_clause" | "else_clause"))
2803        {
2804            if let Some(expression) = clause.child_by_field_name("expression") {
2805                collect_ocaml_callable_outcomes(expression, source, names);
2806            }
2807        }
2808        return;
2809    }
2810    if matches!(node.kind(), "match_expression" | "function_expression") {
2811        let mut cursor = node.walk();
2812        for case in node
2813            .named_children(&mut cursor)
2814            .filter(|child| child.kind() == "match_case")
2815        {
2816            if let Some(body) = case.child_by_field_name("body") {
2817                collect_ocaml_callable_outcomes(body, source, names);
2818            }
2819        }
2820    }
2821}
2822
2823fn ocaml_callable_node_flow(
2824    node: Node<'_>,
2825    source: &str,
2826    caller: &FunctionInfo,
2827    caller_context: &[CallableFlow],
2828    flow_index: &OcamlFlowIndex<'_>,
2829    position: Option<&SourceSpan>,
2830) -> Option<CallableFlow> {
2831    if matches!(node.kind(), "fun_expression" | "function_expression") {
2832        let target = anonymous_callable_symbol_at(
2833            node_text(node, source),
2834            &caller.id,
2835            &flow_index.symbols,
2836            position,
2837        )?;
2838        return Some(CallableFlow {
2839            candidates: BTreeSet::from([target]),
2840            unresolved_reasons: BTreeSet::new(),
2841        });
2842    }
2843    if node.kind() == "application_expression" {
2844        let function = node.child_by_field_name("function")?;
2845        let parts = ocaml_value_path(function, source)?;
2846        let arguments = ocaml_application_arguments(node, function, source);
2847        let Some(target) = flow_index.symbols.resolve(&parts, &caller.id, position) else {
2848            let projection = match parts.as_slice() {
2849                [name] if name == "fst" => Some(0),
2850                [name] if name == "snd" => Some(1),
2851                _ => None,
2852            }?;
2853            let argument = arguments.first()?;
2854            return projected_ocaml_callable_flow(
2855                argument,
2856                &[OcamlProjection::Tuple(projection)],
2857                caller,
2858                caller_context,
2859                flow_index,
2860                position,
2861                0,
2862            );
2863        };
2864        let target_function = flow_index.symbols.get(&target)?;
2865        let target_context = flow_index
2866            .parameters
2867            .get(&target)
2868            .into_iter()
2869            .flatten()
2870            .map(|binding| {
2871                arguments
2872                    .get(binding.argument_index)
2873                    .and_then(|argument| project_ocaml_argument(argument, &binding.projection))
2874                    .map_or_else(opaque_callable_flow, |argument| {
2875                        argument_callable_flow(
2876                            &argument,
2877                            caller,
2878                            caller_context,
2879                            flow_index,
2880                            position,
2881                        )
2882                    })
2883            })
2884            .collect::<Vec<_>>();
2885        let outcomes = flow_index.return_outcomes.get(&target)?;
2886        let mut flow = CallableFlow::default();
2887        for outcome in outcomes {
2888            merge_callable_flow(
2889                &mut flow,
2890                argument_callable_flow(
2891                    outcome,
2892                    target_function,
2893                    &target_context,
2894                    flow_index,
2895                    position,
2896                ),
2897            );
2898        }
2899        return (!outcomes.is_empty()).then_some(flow);
2900    }
2901    if node.kind() == "field_get_expression" {
2902        let record = node.child_by_field_name("record")?;
2903        let field = node.child_by_field_name("field")?;
2904        return ocaml_record_field_flow(
2905            node_text(record, source),
2906            node_text(field, source),
2907            caller,
2908            caller_context,
2909            flow_index,
2910            position,
2911            0,
2912        );
2913    }
2914    if matches!(
2915        node.kind(),
2916        "value_path" | "value_name" | "parenthesized_operator"
2917    ) {
2918        return ocaml_callable_atom_flow(
2919            node_text(node, source),
2920            caller,
2921            caller_context,
2922            flow_index,
2923            position,
2924        );
2925    }
2926    if matches!(
2927        node.kind(),
2928        "expression_item"
2929            | "parenthesized_expression"
2930            | "typed_expression"
2931            | "coercion_expression"
2932            | "let_expression"
2933            | "local_open_expression"
2934    ) {
2935        let expression = node
2936            .child_by_field_name("body")
2937            .or_else(|| node.child_by_field_name("expression"))
2938            .or_else(|| node.named_child(0))?;
2939        return ocaml_callable_node_flow(
2940            expression,
2941            source,
2942            caller,
2943            caller_context,
2944            flow_index,
2945            position,
2946        );
2947    }
2948    if node.kind() == "sequence_expression" {
2949        let mut cursor = node.walk();
2950        let last = node.named_children(&mut cursor).last()?;
2951        return ocaml_callable_node_flow(
2952            last,
2953            source,
2954            caller,
2955            caller_context,
2956            flow_index,
2957            position,
2958        );
2959    }
2960    if node.kind() == "if_expression" {
2961        let mut flow = CallableFlow::default();
2962        let mut branches = 0usize;
2963        let mut cursor = node.walk();
2964        for clause in node
2965            .named_children(&mut cursor)
2966            .filter(|child| matches!(child.kind(), "then_clause" | "else_clause"))
2967        {
2968            let Some(expression) = clause.child_by_field_name("expression") else {
2969                flow.unresolved_reasons
2970                    .insert(UnresolvedReason::UnsupportedConstruct);
2971                continue;
2972            };
2973            branches += 1;
2974            merge_callable_flow(
2975                &mut flow,
2976                ocaml_callable_node_flow(
2977                    expression,
2978                    source,
2979                    caller,
2980                    caller_context,
2981                    flow_index,
2982                    position,
2983                )
2984                .unwrap_or_else(opaque_callable_flow),
2985            );
2986        }
2987        if branches < 2 {
2988            flow.unresolved_reasons
2989                .insert(UnresolvedReason::OpaqueInput);
2990        }
2991        return Some(flow);
2992    }
2993    if matches!(node.kind(), "match_expression" | "function_expression") {
2994        let mut flow = CallableFlow::default();
2995        let mut branches = 0usize;
2996        let mut cursor = node.walk();
2997        for case in node
2998            .named_children(&mut cursor)
2999            .filter(|child| child.kind() == "match_case")
3000        {
3001            let Some(body) = case.child_by_field_name("body") else {
3002                flow.unresolved_reasons
3003                    .insert(UnresolvedReason::UnsupportedConstruct);
3004                continue;
3005            };
3006            branches += 1;
3007            merge_callable_flow(
3008                &mut flow,
3009                ocaml_callable_node_flow(
3010                    body,
3011                    source,
3012                    caller,
3013                    caller_context,
3014                    flow_index,
3015                    position,
3016                )
3017                .unwrap_or_else(opaque_callable_flow),
3018            );
3019        }
3020        return (branches > 0).then_some(flow);
3021    }
3022    None
3023}
3024
3025#[allow(clippy::too_many_arguments)]
3026fn projected_ocaml_callable_flow(
3027    expression: &str,
3028    projection: &[OcamlProjection],
3029    caller: &FunctionInfo,
3030    caller_context: &[CallableFlow],
3031    flow_index: &OcamlFlowIndex<'_>,
3032    position: Option<&SourceSpan>,
3033    depth: usize,
3034) -> Option<CallableFlow> {
3035    if depth >= 64 {
3036        return Some(CallableFlow {
3037            candidates: BTreeSet::new(),
3038            unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
3039        });
3040    }
3041
3042    let expression = normalize_ocaml_callable_argument(expression);
3043    if is_value_identifier(expression)
3044        && let Some(local) =
3045            scoped_ocaml_local_value(flow_index.local_values, &caller.id, expression, position)
3046    {
3047        let definition_position = position_before_ocaml_scope(&local.scope);
3048        return projected_ocaml_callable_flow(
3049            &local.expression,
3050            projection,
3051            caller,
3052            caller_context,
3053            flow_index,
3054            Some(&definition_position),
3055            depth + 1,
3056        );
3057    }
3058
3059    let projected = project_ocaml_argument(expression, projection)?;
3060    Some(argument_callable_flow(
3061        &projected,
3062        caller,
3063        caller_context,
3064        flow_index,
3065        position,
3066    ))
3067}
3068
3069fn ocaml_callable_atom_flow(
3070    argument: &str,
3071    caller: &FunctionInfo,
3072    caller_context: &[CallableFlow],
3073    flow_index: &OcamlFlowIndex<'_>,
3074    position: Option<&SourceSpan>,
3075) -> Option<CallableFlow> {
3076    let argument = normalize_ocaml_callable_argument(argument);
3077    if let Some(local) =
3078        scoped_ocaml_local_value(flow_index.local_values, &caller.id, argument, position)
3079    {
3080        let definition_position = position_before_ocaml_scope(&local.scope);
3081        return Some(
3082            parse_ocaml_callable_flow(
3083                &local.expression,
3084                caller,
3085                caller_context,
3086                flow_index,
3087                Some(&definition_position),
3088            )
3089            .unwrap_or_else(opaque_callable_flow),
3090        );
3091    }
3092    if let Some(flow) = flow_index
3093        .parameters
3094        .get(&caller.id)
3095        .and_then(|parameters| {
3096            parameters.iter().position(|parameter| {
3097                parameter.kind == OcamlParameterKind::Value
3098                    && parameter.name == argument
3099                    && parameter.scope.as_ref().is_none_or(|scope| {
3100                        position.is_some_and(|position| scope.contains(position))
3101                    })
3102            })
3103        })
3104        .map(|index| {
3105            caller_context
3106                .get(index)
3107                .cloned()
3108                .unwrap_or_else(opaque_callable_flow)
3109        })
3110    {
3111        return Some(flow);
3112    }
3113    if let Some(target) = local_callable_at(
3114        argument,
3115        &caller.id,
3116        &flow_index.symbols,
3117        flow_index.local_callable_scopes,
3118        position,
3119    ) {
3120        return Some(CallableFlow {
3121            candidates: BTreeSet::from([target]),
3122            unresolved_reasons: BTreeSet::new(),
3123        });
3124    }
3125    argument_callable_symbol(argument, &caller.id, &flow_index.symbols, position).map(|target| {
3126        CallableFlow {
3127            candidates: BTreeSet::from([target]),
3128            unresolved_reasons: BTreeSet::new(),
3129        }
3130    })
3131}
3132
3133#[allow(clippy::too_many_arguments)]
3134fn ocaml_record_field_flow(
3135    record_expression: &str,
3136    requested_field: &str,
3137    caller: &FunctionInfo,
3138    caller_context: &[CallableFlow],
3139    flow_index: &OcamlFlowIndex<'_>,
3140    position: Option<&SourceSpan>,
3141    depth: usize,
3142) -> Option<CallableFlow> {
3143    if depth >= 64 {
3144        return Some(CallableFlow {
3145            candidates: BTreeSet::new(),
3146            unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
3147        });
3148    }
3149
3150    let record_expression = strip_outer_ocaml_parentheses(record_expression).trim();
3151    if is_value_identifier(record_expression)
3152        && let Some(local) = scoped_ocaml_local_value(
3153            flow_index.local_values,
3154            &caller.id,
3155            record_expression,
3156            position,
3157        )
3158    {
3159        let definition_position = position_before_ocaml_scope(&local.scope);
3160        return ocaml_record_field_flow(
3161            &local.expression,
3162            requested_field,
3163            caller,
3164            caller_context,
3165            flow_index,
3166            Some(&definition_position),
3167            depth + 1,
3168        );
3169    }
3170
3171    let tree = parse_ocaml_expression(record_expression)?;
3172    if tree.root_node().has_error() {
3173        return None;
3174    }
3175    let mut record = tree.root_node().named_child(0)?;
3176    while matches!(
3177        record.kind(),
3178        "expression_item" | "parenthesized_expression"
3179    ) {
3180        record = record
3181            .child_by_field_name("expression")
3182            .or_else(|| record.named_child(0))?;
3183    }
3184    if record.kind() != "record_expression" {
3185        return None;
3186    }
3187
3188    let requested_field = normalize_source(requested_field);
3189    let requested_field = requested_field
3190        .rsplit('.')
3191        .next()
3192        .unwrap_or(&requested_field);
3193    let mut cursor = record.walk();
3194    for field in record
3195        .named_children(&mut cursor)
3196        .filter(|child| child.kind() == "field_expression")
3197    {
3198        let Some(path) = field
3199            .named_children(&mut field.walk())
3200            .find(|child| child.kind() == "field_path")
3201        else {
3202            continue;
3203        };
3204        let field_name = normalize_source(node_text(path, record_expression));
3205        if field_name.rsplit('.').next() != Some(requested_field) {
3206            continue;
3207        }
3208        if let Some(body) = field.child_by_field_name("body") {
3209            return ocaml_callable_node_flow(
3210                body,
3211                record_expression,
3212                caller,
3213                caller_context,
3214                flow_index,
3215                position,
3216            );
3217        }
3218        return ocaml_callable_atom_flow(&field_name, caller, caller_context, flow_index, position);
3219    }
3220
3221    let base = record.child_by_field_name("record")?;
3222    ocaml_record_field_flow(
3223        node_text(base, record_expression),
3224        requested_field,
3225        caller,
3226        caller_context,
3227        flow_index,
3228        position,
3229        depth + 1,
3230    )
3231}
3232
3233fn ocaml_application_arguments(
3234    application: Node<'_>,
3235    function: Node<'_>,
3236    source: &str,
3237) -> Vec<String> {
3238    let application = normalize_source(node_text(application, source));
3239    let function = normalize_source(node_text(function, source));
3240    application
3241        .strip_prefix(&function)
3242        .map(str::trim)
3243        .map(split_ocaml_arguments)
3244        .unwrap_or_default()
3245}
3246
3247fn opaque_callable_flow() -> CallableFlow {
3248    CallableFlow {
3249        candidates: BTreeSet::new(),
3250        unresolved_reasons: [UnresolvedReason::OpaqueInput].into_iter().collect(),
3251    }
3252}
3253
3254fn merge_callable_flow(destination: &mut CallableFlow, source: CallableFlow) {
3255    destination.candidates.extend(source.candidates);
3256    destination
3257        .unresolved_reasons
3258        .extend(source.unresolved_reasons);
3259}
3260
3261fn argument_callable_symbol(
3262    argument: &str,
3263    caller: &SymbolId,
3264    symbols: &OcamlSymbols<'_>,
3265    position: Option<&SourceSpan>,
3266) -> Option<SymbolId> {
3267    let argument = normalize_ocaml_callable_argument(argument);
3268    if let Some(candidate) = anonymous_callable_symbol(argument, caller, symbols) {
3269        return Some(candidate);
3270    }
3271    if let Some(candidate) =
3272        displayed_anonymous_callable_symbol(argument, caller, symbols.functions)
3273    {
3274        return Some(candidate);
3275    }
3276    let parts = argument
3277        .split('.')
3278        .map(ToOwned::to_owned)
3279        .collect::<Vec<_>>();
3280    symbols.resolve(&parts, caller, position)
3281}
3282
3283fn displayed_anonymous_callable_symbol(
3284    expression: &str,
3285    caller: &SymbolId,
3286    functions: &[FunctionInfo],
3287) -> Option<SymbolId> {
3288    let lambda = normalize_ocaml_callable_argument(expression);
3289    if !lambda.starts_with('λ') {
3290        return None;
3291    }
3292    let direct_owner = caller.to_string();
3293    let sibling_owner = caller.container.as_deref();
3294    let mut candidates = functions
3295        .iter()
3296        .filter(|candidate| {
3297            local_callable_source_name(candidate).starts_with("{lambda:")
3298                && candidate.label.default.split_whitespace().next() == Some(lambda)
3299                && (candidate.id.container.as_deref() == Some(direct_owner.as_str())
3300                    || candidate.id.container.as_deref() == sibling_owner)
3301        })
3302        .map(|candidate| candidate.id.clone())
3303        .collect::<Vec<_>>();
3304    candidates.sort();
3305    candidates.dedup();
3306    (candidates.len() == 1).then(|| candidates.remove(0))
3307}
3308
3309fn anonymous_callable_symbol(
3310    expression: &str,
3311    caller: &SymbolId,
3312    symbols: &OcamlSymbols<'_>,
3313) -> Option<SymbolId> {
3314    anonymous_callable_symbol_at(expression, caller, symbols, None)
3315}
3316
3317fn anonymous_callable_symbol_at(
3318    expression: &str,
3319    caller: &SymbolId,
3320    symbols: &OcamlSymbols<'_>,
3321    position: Option<&SourceSpan>,
3322) -> Option<SymbolId> {
3323    let base = anonymous_callable_base(expression)?;
3324    let direct_owner = caller.to_string();
3325    let sibling_owner = caller.container.as_deref();
3326    let mut candidates = symbols
3327        .named(&base)
3328        .filter(|candidate| {
3329            candidate.id.container.as_deref() == Some(direct_owner.as_str())
3330                || candidate.id.container.as_deref() == sibling_owner
3331        })
3332        .map(|candidate| candidate.id.clone())
3333        .collect::<Vec<_>>();
3334    if let Some(position) = position {
3335        let positioned = candidates
3336            .iter()
3337            .filter(|candidate| {
3338                symbols
3339                    .get(candidate)
3340                    .is_some_and(|function| position.contains(&function.span))
3341            })
3342            .cloned()
3343            .collect::<Vec<_>>();
3344        if !positioned.is_empty() {
3345            candidates = positioned;
3346        }
3347    }
3348    candidates.sort();
3349    candidates.dedup();
3350    (candidates.len() == 1).then(|| candidates.remove(0))
3351}
3352
3353fn normalize_ocaml_callable_argument(argument: &str) -> &str {
3354    let argument = argument.trim();
3355    let argument = argument
3356        .strip_prefix(['~', '?'])
3357        .map_or(argument, |argument| {
3358            argument
3359                .split_once(':')
3360                .map_or(argument, |(_, value)| value)
3361        });
3362    strip_outer_ocaml_parentheses(argument).trim()
3363}
3364
3365fn ocaml_call_arguments(call: &CallSite) -> Vec<String> {
3366    let callee = match call.syntax.visible() {
3367        CallSyntax::Path(parts) => parts.join("."),
3368        CallSyntax::SelfMethod(method) => method.clone(),
3369        CallSyntax::Method { receiver, method } => format!("{receiver}#{method}"),
3370        CallSyntax::Expression(expression) => expression.clone(),
3371        CallSyntax::CompilerConfirmed(syntax) => syntax.key_fragment(),
3372    };
3373    let remainder = call
3374        .label
3375        .default
3376        .strip_prefix(&callee)
3377        .unwrap_or(&call.label.default)
3378        .trim();
3379    split_ocaml_arguments(remainder)
3380}
3381
3382fn split_ocaml_arguments(arguments: &str) -> Vec<String> {
3383    let mut result = Vec::new();
3384    let mut start = None;
3385    let mut delimiters = Vec::new();
3386    let mut in_string = false;
3387    let mut escaped = false;
3388    for (index, character) in arguments.char_indices() {
3389        if in_string {
3390            if escaped {
3391                escaped = false;
3392            } else if character == '\\' {
3393                escaped = true;
3394            } else if character == '"' {
3395                in_string = false;
3396            }
3397            continue;
3398        }
3399        match character {
3400            '"' => {
3401                in_string = true;
3402                start.get_or_insert(index);
3403            }
3404            '(' | '[' | '{' => {
3405                delimiters.push(character);
3406                start.get_or_insert(index);
3407            }
3408            ')' | ']' | '}' => {
3409                delimiters.pop();
3410            }
3411            character if character.is_whitespace() && delimiters.is_empty() => {
3412                if let Some(argument_start) = start.take() {
3413                    result.push(arguments[argument_start..index].to_owned());
3414                }
3415            }
3416            _ => {
3417                start.get_or_insert(index);
3418            }
3419        }
3420    }
3421    if let Some(argument_start) = start {
3422        result.push(arguments[argument_start..].to_owned());
3423    }
3424    result
3425}
3426
3427fn ocaml_candidate_call_label(function: &FunctionInfo, arguments: &[String]) -> String {
3428    let mut name = local_callable_source_name(function).to_owned();
3429    let anonymous = name.starts_with("{lambda:");
3430    if anonymous {
3431        name = function
3432            .label
3433            .default
3434            .split_whitespace()
3435            .next()
3436            .unwrap_or("λ")
3437            .to_owned();
3438    }
3439    if function.id.container.is_none() && !function.id.module.is_empty() {
3440        name = function
3441            .id
3442            .module
3443            .iter()
3444            .chain(std::iter::once(&name))
3445            .cloned()
3446            .collect::<Vec<_>>()
3447            .join(".");
3448    }
3449    let mut label = if arguments.is_empty() {
3450        name
3451    } else {
3452        format!("{name} {}", arguments.join(" "))
3453    };
3454    if !anonymous
3455        && let Some(ordinal) = function
3456            .id
3457            .name
3458            .rsplit_once("#closure")
3459            .map(|(_, ordinal)| ordinal)
3460    {
3461        label.push_str(&format!(" [closure#{ordinal}]"));
3462    }
3463    label
3464}
3465
3466fn function_from_binding(
3467    file: &Path,
3468    module: &[String],
3469    binding: Node<'_>,
3470    source: &str,
3471    analysis: &mut FileAnalysis,
3472) -> Option<FunctionInfo> {
3473    let pattern = binding.child_by_field_name("pattern")?;
3474    if !matches!(pattern.kind(), "value_name" | "value_pattern") {
3475        return None;
3476    }
3477    let name = node_text(pattern, source).trim();
3478    if !is_value_identifier(name) {
3479        return None;
3480    }
3481
3482    let binding_body = binding.child_by_field_name("body")?;
3483    let mut parameters = direct_named_children(binding, "parameter");
3484    let mut body = binding_body;
3485    let mut function_expression = false;
3486    while body.kind() == "fun_expression" {
3487        function_expression = true;
3488        parameters.extend(direct_named_children(body, "parameter"));
3489        body = body.child_by_field_name("body").unwrap_or(body);
3490    }
3491    if body.kind() == "function_expression" {
3492        function_expression = true;
3493        parameters.push(body);
3494    }
3495    if parameters.is_empty() && !function_expression {
3496        return None;
3497    }
3498
3499    let id = SymbolId {
3500        language: LanguageId::new("ocaml"),
3501        module: module.to_vec(),
3502        container: None,
3503        name: name.to_owned(),
3504    };
3505    let parameter_labels = parameters
3506        .iter()
3507        .map(|parameter| {
3508            if parameter.kind() == "function_expression" {
3509                "_".to_owned()
3510            } else {
3511                parameter_default_label(*parameter, source)
3512            }
3513        })
3514        .collect::<Vec<_>>();
3515    let default_label = ocaml_declaration_label(&id, &parameter_labels);
3516    let typed_label = typed_declaration_label(&id, &parameters, source);
3517
3518    for (index, parameter) in parameters.iter().enumerate() {
3519        if parameter.kind() == "function_expression" {
3520            continue;
3521        }
3522        let key = parameter_labels[index].clone();
3523        let value = parameter_pattern_and_type(*parameter)
3524            .1
3525            .map(|node| normalize_source(node_text(node, source)))
3526            .unwrap_or_default();
3527        analysis.facts.push(LanguageFact {
3528            subject: id.clone(),
3529            namespace: LanguageId::new("ocaml"),
3530            kind: "parameter".to_owned(),
3531            key,
3532            value,
3533            span: tree_sitter_span(file, *parameter),
3534        });
3535    }
3536
3537    let mut return_outcomes = BTreeSet::new();
3538    collect_ocaml_callable_outcomes(body, source, &mut return_outcomes);
3539    for (index, outcome) in return_outcomes.into_iter().enumerate() {
3540        analysis.facts.push(LanguageFact {
3541            subject: id.clone(),
3542            namespace: LanguageId::new("ocaml"),
3543            kind: "callable-return".to_owned(),
3544            key: index.to_string(),
3545            value: outcome,
3546            span: tree_sitter_span(file, body),
3547        });
3548    }
3549
3550    collect_ocaml_local_value_facts(file, &id, body, source, true, &mut analysis.facts);
3551    collect_ocaml_parameter_refinement_facts(file, &id, body, source, true, &mut analysis.facts);
3552
3553    let mut calls = Vec::new();
3554    collect_calls(file, body, source, true, &mut calls);
3555
3556    Some(FunctionInfo {
3557        id,
3558        label: match typed_label {
3559            Some(typed) => CallLabel::with_types(default_label, typed),
3560            None => CallLabel::new(default_label),
3561        },
3562        // Without an .mli index, structure bindings are conservatively treated
3563        // as exported entry candidates. Explicit --entry remains exact.
3564        public: true,
3565        calls,
3566        span: tree_sitter_span(file, binding),
3567    })
3568}
3569
3570fn collect_ocaml_local_value_facts(
3571    file: &Path,
3572    owner: &SymbolId,
3573    node: Node<'_>,
3574    source: &str,
3575    is_root: bool,
3576    facts: &mut Vec<LanguageFact>,
3577) {
3578    if !is_root && matches!(node.kind(), "fun_expression" | "function_expression") {
3579        return;
3580    }
3581    if node.kind() == "let_expression" {
3582        let bindings = direct_named_children(node, "value_definition")
3583            .into_iter()
3584            .flat_map(|definition| direct_named_children(definition, "let_binding"));
3585        for binding in bindings {
3586            let Some(body) = binding.child_by_field_name("body") else {
3587                continue;
3588            };
3589            let is_callable_binding = !direct_named_children(binding, "parameter").is_empty()
3590                || matches!(body.kind(), "fun_expression" | "function_expression");
3591            if is_callable_binding {
3592                continue;
3593            }
3594            if let Some(pattern) = binding.child_by_field_name("pattern") {
3595                let mut bindings = Vec::new();
3596                collect_ocaml_parameter_bindings(
3597                    node_text(pattern, source),
3598                    0,
3599                    &mut Vec::new(),
3600                    &mut bindings,
3601                );
3602                for local in bindings
3603                    .into_iter()
3604                    .filter(|binding| binding.kind == OcamlParameterKind::Value)
3605                {
3606                    let Some(value) =
3607                        project_ocaml_argument(node_text(body, source), &local.projection)
3608                    else {
3609                        continue;
3610                    };
3611                    facts.push(LanguageFact {
3612                        subject: owner.clone(),
3613                        namespace: LanguageId::new("ocaml"),
3614                        kind: "local-value".to_owned(),
3615                        key: local.name,
3616                        value,
3617                        span: tree_sitter_span(
3618                            file,
3619                            node.child_by_field_name("body").unwrap_or(node),
3620                        ),
3621                    });
3622                }
3623            }
3624        }
3625    }
3626
3627    let mut cursor = node.walk();
3628    for child in node.named_children(&mut cursor) {
3629        collect_ocaml_local_value_facts(file, owner, child, source, false, facts);
3630    }
3631}
3632
3633fn collect_ocaml_parameter_refinement_facts(
3634    file: &Path,
3635    owner: &SymbolId,
3636    node: Node<'_>,
3637    source: &str,
3638    is_root: bool,
3639    facts: &mut Vec<LanguageFact>,
3640) {
3641    if !is_root && matches!(node.kind(), "fun_expression" | "function_expression") {
3642        return;
3643    }
3644    if node.kind() == "let_binding" && !direct_named_children(node, "parameter").is_empty() {
3645        return;
3646    }
3647
3648    if node.kind() == "match_expression"
3649        && let Some(scrutinee) = node.child_by_field_name("expression")
3650    {
3651        let scrutinee = normalize_source(node_text(scrutinee, source));
3652        if is_value_identifier(&scrutinee) {
3653            for case in direct_named_children(node, "match_case") {
3654                let (Some(pattern), Some(body)) = (
3655                    case.child_by_field_name("pattern"),
3656                    case.child_by_field_name("body"),
3657                ) else {
3658                    continue;
3659                };
3660                push_ocaml_parameter_refinements(
3661                    file,
3662                    owner,
3663                    &scrutinee,
3664                    node_text(pattern, source),
3665                    body,
3666                    facts,
3667                );
3668            }
3669        }
3670    }
3671
3672    if node.kind() == "let_expression"
3673        && let Some(scope) = node.child_by_field_name("body")
3674    {
3675        for binding in direct_named_children(node, "value_definition")
3676            .into_iter()
3677            .flat_map(|definition| direct_named_children(definition, "let_binding"))
3678        {
3679            let (Some(pattern), Some(value)) = (
3680                binding.child_by_field_name("pattern"),
3681                binding.child_by_field_name("body"),
3682            ) else {
3683                continue;
3684            };
3685            let value = normalize_source(node_text(value, source));
3686            if is_value_identifier(&value) {
3687                push_ocaml_parameter_refinements(
3688                    file,
3689                    owner,
3690                    &value,
3691                    node_text(pattern, source),
3692                    scope,
3693                    facts,
3694                );
3695            }
3696        }
3697    }
3698
3699    let mut cursor = node.walk();
3700    for child in node.named_children(&mut cursor) {
3701        collect_ocaml_parameter_refinement_facts(file, owner, child, source, false, facts);
3702    }
3703}
3704
3705fn push_ocaml_parameter_refinements(
3706    file: &Path,
3707    owner: &SymbolId,
3708    source_name: &str,
3709    pattern: &str,
3710    scope: Node<'_>,
3711    facts: &mut Vec<LanguageFact>,
3712) {
3713    let mut bindings = Vec::new();
3714    collect_ocaml_parameter_bindings(pattern, 0, &mut Vec::new(), &mut bindings);
3715    for binding in bindings {
3716        if binding.kind != OcamlParameterKind::Value {
3717            continue;
3718        }
3719        facts.push(LanguageFact {
3720            subject: owner.clone(),
3721            namespace: LanguageId::new("ocaml"),
3722            kind: "parameter-refinement".to_owned(),
3723            key: binding.name,
3724            value: format!("{source_name}\n{}", normalize_source(pattern)),
3725            span: tree_sitter_span(file, scope),
3726        });
3727    }
3728}
3729
3730fn collect_calls(
3731    file: &Path,
3732    node: Node<'_>,
3733    source: &str,
3734    is_callable_body: bool,
3735    calls: &mut Vec<CallSite>,
3736) {
3737    if node.kind() == "application_expression" {
3738        // Evaluate a callable expression and its arguments before recording
3739        // the enclosing application. This preserves source execution order
3740        // for `(make_callback x) y` and mirrors the compiler event order.
3741        let mut cursor = node.walk();
3742        for child in node.named_children(&mut cursor) {
3743            collect_calls(file, child, source, false, calls);
3744        }
3745        if let Some(function) = node.child_by_field_name("function") {
3746            if matches!(
3747                function.kind(),
3748                "constructor_path" | "constructor_name" | "tag"
3749            ) {
3750                return;
3751            }
3752            if let Some(parts) = ocaml_value_path(function, source) {
3753                if ocaml_path_is_constructor(&parts) {
3754                    return;
3755                }
3756                let label = ocaml_path_call_label(node, function, &parts, source);
3757                let syntax = CallSyntax::Path(parts);
3758                let span = tree_sitter_span(file, node);
3759                calls.push(CallSite {
3760                    id: CallSiteId::source(&syntax, &span),
3761                    syntax,
3762                    target: CallTarget::Unresolved,
3763                    label: CallLabel::new(label),
3764                    span,
3765                });
3766            } else if function.kind() == "method_invocation" {
3767                let syntax = CallSyntax::Path(vec![normalize_source(node_text(function, source))]);
3768                let span = tree_sitter_span(file, node);
3769                calls.push(CallSite {
3770                    id: CallSiteId::source(&syntax, &span),
3771                    syntax,
3772                    target: CallTarget::Dynamic {
3773                        dispatch: SymbolId {
3774                            language: LanguageId::new("ocaml"),
3775                            module: Vec::new(),
3776                            container: None,
3777                            name: format!(
3778                                "object-method@{}:{}",
3779                                node.start_position().row + 1,
3780                                node.start_position().column
3781                            ),
3782                        },
3783                        candidates: Vec::new(),
3784                        resolution: DispatchResolution::Unresolved,
3785                        evidence: DispatchEvidence::ExactFlow,
3786                        unresolved_reasons: [UnresolvedReason::OpaqueInput].into_iter().collect(),
3787                    },
3788                    label: CallLabel::new(normalize_source(node_text(node, source))),
3789                    span,
3790                });
3791            } else {
3792                let syntax = CallSyntax::Expression(normalize_source(node_text(function, source)));
3793                let span = tree_sitter_span(file, node);
3794                calls.push(CallSite {
3795                    id: CallSiteId::source(&syntax, &span),
3796                    syntax,
3797                    target: CallTarget::Unresolved,
3798                    label: CallLabel::new(normalize_source(node_text(node, source))),
3799                    span,
3800                });
3801            }
3802        }
3803        return;
3804    } else if !is_callable_body && matches!(node.kind(), "fun_expression" | "function_expression") {
3805        // A nested closure owns its calls. It will receive its own callable ID
3806        // when local callable extraction is added.
3807        return;
3808    } else if node.kind() == "let_binding" && !direct_named_children(node, "parameter").is_empty() {
3809        // Likewise, do not attribute a local function's body to its enclosing
3810        // function.
3811        return;
3812    }
3813
3814    let mut cursor = node.walk();
3815    for child in node.named_children(&mut cursor) {
3816        collect_calls(file, child, source, false, calls);
3817    }
3818}
3819
3820fn ocaml_path_is_constructor(parts: &[String]) -> bool {
3821    parts.last().is_some_and(|name| {
3822        name.starts_with('`')
3823            || name
3824                .chars()
3825                .next()
3826                .is_some_and(|character| character.is_ascii_uppercase())
3827    })
3828}
3829
3830fn ocaml_path_call_label(
3831    application: Node<'_>,
3832    function: Node<'_>,
3833    parts: &[String],
3834    source: &str,
3835) -> String {
3836    let application = normalize_source(node_text(application, source));
3837    let function = normalize_source(node_text(function, source));
3838    let qualified = parts.join(".");
3839    application
3840        .strip_prefix(&function)
3841        .map_or(application.clone(), |arguments| {
3842            format!("{qualified}{arguments}")
3843        })
3844}
3845
3846fn ocaml_value_path(function: Node<'_>, source: &str) -> Option<Vec<String>> {
3847    if !matches!(
3848        function.kind(),
3849        "value_path" | "value_name" | "parenthesized_operator"
3850    ) {
3851        return None;
3852    }
3853    let raw = normalize_source(node_text(function, source));
3854    let mut parts = raw
3855        .split('.')
3856        .map(str::trim)
3857        .filter(|part| !part.is_empty())
3858        .map(ToOwned::to_owned)
3859        .collect::<Vec<_>>();
3860    let mut opened = Vec::new();
3861    let mut ancestor = function.parent();
3862    while let Some(node) = ancestor {
3863        if node.kind() == "local_open_expression"
3864            && let Some(module_path) = direct_named_child(node, "module_path")
3865        {
3866            let mut prefix = normalize_source(node_text(module_path, source))
3867                .split('.')
3868                .map(str::trim)
3869                .filter(|part| !part.is_empty())
3870                .map(ToOwned::to_owned)
3871                .collect::<Vec<_>>();
3872            prefix.append(&mut opened);
3873            opened = prefix;
3874        }
3875        ancestor = node.parent();
3876    }
3877    opened.append(&mut parts);
3878    let parts = opened;
3879    (!parts.is_empty()).then_some(parts)
3880}
3881
3882fn direct_named_child<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
3883    let mut cursor = node.walk();
3884    node.named_children(&mut cursor)
3885        .find(|child| child.kind() == kind)
3886}
3887
3888fn direct_named_children<'tree>(node: Node<'tree>, kind: &str) -> Vec<Node<'tree>> {
3889    let mut cursor = node.walk();
3890    node.named_children(&mut cursor)
3891        .filter(|child| child.kind() == kind)
3892        .collect()
3893}
3894
3895fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
3896    node.utf8_text(source.as_bytes())
3897        .expect("tree-sitter nodes always point into the parsed UTF-8 source")
3898}
3899
3900fn ocaml_declaration_label(id: &SymbolId, parameters: &[String]) -> String {
3901    let name = id
3902        .module
3903        .iter()
3904        .chain(std::iter::once(&id.name))
3905        .cloned()
3906        .collect::<Vec<_>>()
3907        .join(".");
3908    if parameters.is_empty() {
3909        name
3910    } else {
3911        format!("{name} {}", parameters.join(" "))
3912    }
3913}
3914
3915fn typed_declaration_label(id: &SymbolId, parameters: &[Node<'_>], source: &str) -> Option<String> {
3916    let mut found_type = false;
3917    let labels = parameters
3918        .iter()
3919        .map(|parameter| {
3920            if parameter.kind() == "function_expression" {
3921                return "_".to_owned();
3922            }
3923            let (label, typed) = parameter_typed_label(*parameter, source);
3924            found_type |= typed;
3925            label
3926        })
3927        .collect::<Vec<_>>();
3928    found_type.then(|| ocaml_declaration_label(id, &labels))
3929}
3930
3931fn parameter_default_label(parameter: Node<'_>, source: &str) -> String {
3932    let (pattern_node, ty) = parameter_pattern_and_type(parameter);
3933    if ty.is_none() {
3934        return normalize_source(node_text(parameter, source));
3935    }
3936
3937    let pattern = pattern_node
3938        .map(|pattern| normalize_source(node_text(pattern, source)))
3939        .unwrap_or_else(|| "_".to_owned());
3940    let Some(label) = direct_named_child(parameter, "label_name") else {
3941        return pattern;
3942    };
3943    let marker = node_text(parameter, source)
3944        .trim_start()
3945        .chars()
3946        .next()
3947        .filter(|marker| matches!(marker, '~' | '?'))
3948        .unwrap_or('~');
3949    let label = normalize_source(node_text(label, source));
3950    if pattern == label {
3951        format!("{marker}{label}")
3952    } else {
3953        format!("{marker}{label}:{pattern}")
3954    }
3955}
3956
3957fn parameter_typed_label(parameter: Node<'_>, source: &str) -> (String, bool) {
3958    let (pattern_node, ty) = parameter_pattern_and_type(parameter);
3959    let Some(ty) = ty else {
3960        return (parameter_default_label(parameter, source), false);
3961    };
3962    let pattern = pattern_node
3963        .map(|pattern| normalize_source(node_text(pattern, source)))
3964        .unwrap_or_else(|| "_".to_owned());
3965    let annotated = format!("({pattern} : {})", normalize_source(node_text(ty, source)));
3966    let Some(label) = direct_named_child(parameter, "label_name") else {
3967        return (annotated, true);
3968    };
3969    let marker = node_text(parameter, source)
3970        .trim_start()
3971        .chars()
3972        .next()
3973        .filter(|marker| matches!(marker, '~' | '?'))
3974        .unwrap_or('~');
3975    (
3976        format!(
3977            "{marker}{}:{annotated}",
3978            normalize_source(node_text(label, source))
3979        ),
3980        true,
3981    )
3982}
3983
3984fn parameter_pattern_and_type<'tree>(
3985    parameter: Node<'tree>,
3986) -> (Option<Node<'tree>>, Option<Node<'tree>>) {
3987    let pattern = parameter.child_by_field_name("pattern");
3988    match pattern {
3989        Some(pattern) if pattern.kind() == "typed_pattern" => (
3990            pattern.child_by_field_name("pattern"),
3991            pattern.child_by_field_name("type"),
3992        ),
3993        pattern => (pattern, parameter.child_by_field_name("type")),
3994    }
3995}
3996
3997fn is_value_identifier(value: &str) -> bool {
3998    let mut chars = value.chars();
3999    chars
4000        .next()
4001        .is_some_and(|first| first == '_' || first.is_ascii_lowercase())
4002        && chars.all(|character| {
4003            character == '_' || character == '\'' || character.is_ascii_alphanumeric()
4004        })
4005}
4006
4007fn tree_sitter_span(file: &Path, node: Node<'_>) -> SourceSpan {
4008    SourceSpan {
4009        file: file.to_path_buf(),
4010        start_line: node.start_position().row + 1,
4011        start_column: node.start_position().column,
4012        start_byte: Some(node.start_byte()),
4013        end_line: node.end_position().row + 1,
4014        end_column: node.end_position().column,
4015        end_byte: Some(node.end_byte()),
4016    }
4017}
4018
4019fn normalize_source(source: &str) -> String {
4020    let mut output = String::with_capacity(source.len());
4021    let mut in_string = false;
4022    let mut escaped = false;
4023    let mut pending_space = false;
4024
4025    for character in source.chars() {
4026        if in_string {
4027            output.push(character);
4028            if escaped {
4029                escaped = false;
4030            } else if character == '\\' {
4031                escaped = true;
4032            } else if character == '"' {
4033                in_string = false;
4034            }
4035            continue;
4036        }
4037
4038        if character == '"' {
4039            if pending_space && !output.is_empty() {
4040                output.push(' ');
4041            }
4042            pending_space = false;
4043            in_string = true;
4044            output.push(character);
4045        } else if character.is_whitespace() {
4046            pending_space = true;
4047        } else {
4048            if pending_space && !output.is_empty() {
4049                output.push(' ');
4050            }
4051            pending_space = false;
4052            output.push(character);
4053        }
4054    }
4055    output.trim().to_owned()
4056}
4057
4058#[cfg(test)]
4059mod tests {
4060    use std::path::Path;
4061
4062    use super::*;
4063
4064    #[test]
4065    fn extracts_module_functions_and_native_call_labels() {
4066        let source = include_str!("../../examples/ocaml/after.ml");
4067        let analysis = OcamlFrontend
4068            .analyze_file(
4069                &FileContext {
4070                    path: Path::new("after.ml"),
4071                    module: &[],
4072                },
4073                source,
4074            )
4075            .unwrap();
4076
4077        let save = analysis
4078            .functions
4079            .iter()
4080            .find(|function| function.id.module == ["Postgres"] && function.id.name == "save")
4081            .unwrap();
4082        assert_eq!(save.label.default, "Postgres.save order");
4083        assert_eq!(
4084            save.calls
4085                .iter()
4086                .map(|call| call.label.default.as_str())
4087                .collect::<Vec<_>>(),
4088            ["Sql.begin_tx order", "Sql.insert order", "Sql.commit order"]
4089        );
4090    }
4091
4092    #[test]
4093    fn keeps_labeled_optional_and_unit_application_syntax() {
4094        let source = r#"
4095            let run order =
4096              charge ~currency:"KRW" order 100;
4097              find ?limit:None ~tenant:"acme" order;
4098              commit ()
4099        "#;
4100        let analysis = OcamlFrontend
4101            .analyze_file(
4102                &FileContext {
4103                    path: Path::new("calls.ml"),
4104                    module: &[],
4105                },
4106                source,
4107            )
4108            .unwrap();
4109        let calls = &analysis.functions[0].calls;
4110        assert_eq!(calls[0].label.default, "charge ~currency:\"KRW\" order 100");
4111        assert_eq!(
4112            calls[1].label.default,
4113            "find ?limit:None ~tenant:\"acme\" order"
4114        );
4115        assert_eq!(calls[2].label.default, "commit ()");
4116    }
4117
4118    #[test]
4119    fn separates_ocaml_parameter_types_from_the_default_label() {
4120        let source = "let run (order : Order.t) = validate order";
4121        let analysis = OcamlFrontend
4122            .analyze_file(
4123                &FileContext {
4124                    path: Path::new("typed.ml"),
4125                    module: &[],
4126                },
4127                source,
4128            )
4129            .unwrap();
4130        let label = &analysis.functions[0].label;
4131        assert_eq!(label.default, "run order");
4132        assert_eq!(label.typed.as_deref(), Some("run (order : Order.t)"));
4133    }
4134
4135    #[test]
4136    fn source_project_resolves_calls_across_ocaml_modules() {
4137        let sequence = OCAML_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
4138        let directory = std::env::temp_dir().join(format!(
4139            "diffkit-ocaml-source-project-{}-{sequence}",
4140            std::process::id()
4141        ));
4142        fs::create_dir(&directory).unwrap();
4143        fs::write(directory.join("a.ml"), "let entry value = B.save value\n").unwrap();
4144        fs::write(directory.join("b.ml"), "let save value = value\n").unwrap();
4145
4146        let analysis = analyze_source_project(&directory).unwrap();
4147        let entry = analysis
4148            .functions
4149            .iter()
4150            .find(|function| function.id.module == ["A"] && function.id.name == "entry")
4151            .unwrap();
4152        assert!(matches!(
4153            &entry.calls[0].target,
4154            CallTarget::Direct(target) if target.module == ["B"] && target.name == "save"
4155        ));
4156        fs::remove_dir_all(directory).unwrap();
4157    }
4158
4159    #[test]
4160    fn maps_dune_build_locations_back_to_source_files() {
4161        let sequence = OCAML_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
4162        let directory = std::env::temp_dir().join(format!(
4163            "diffkit-ocaml-dune-path-{}-{sequence}",
4164            std::process::id()
4165        ));
4166        fs::create_dir_all(directory.join("src")).unwrap();
4167        fs::write(directory.join("src/service.ml"), "let run () = ()\n").unwrap();
4168        let mapped = dune_source_path(&directory, &directory.join("_build/default/src/service.ml"));
4169        assert_eq!(mapped, Some(directory.join("src/service.ml")));
4170        fs::remove_dir_all(directory).unwrap();
4171    }
4172
4173    #[test]
4174    fn compiler_libs_adapter_compiles_when_ocaml_is_installed() {
4175        if Command::new("ocamlc").arg("-version").output().is_err() {
4176            return;
4177        }
4178        let extractor = OcamlExtractor::compile().unwrap();
4179        assert!(extractor.executable().is_file());
4180    }
4181
4182    #[test]
4183    fn splits_curried_inferred_types_without_splitting_nested_arrows() {
4184        assert_eq!(
4185            split_ocaml_function_type("('a -> 'b) -> label:int -> 'a -> 'b"),
4186            ["('a -> 'b)", "label:int", "'a", "'b"]
4187        );
4188    }
4189
4190    #[test]
4191    fn typedtree_identifiers_do_not_turn_callable_parameters_into_static_functions() {
4192        let source = "let apply callback value = callback value";
4193        let path = Path::new("callback.ml");
4194        let mut analysis =
4195            analyze_ocaml_syntax(&FileContext { path, module: &[] }, source).unwrap();
4196        let span = analysis.functions[0].calls[0].span.clone();
4197        apply_typed_events(
4198            &mut analysis,
4199            &[TypedCallEvent {
4200                target: Some("callback".to_owned()),
4201                signature: Some("'a -> 'b".to_owned()),
4202                span,
4203            }],
4204        );
4205
4206        assert!(matches!(
4207            analysis.functions[0].calls[0].target,
4208            CallTarget::Unresolved
4209        ));
4210    }
4211
4212    #[test]
4213    fn one_typedtree_event_cannot_be_reused_by_two_source_calls() {
4214        let source = "let run value = outer (inner value)";
4215        let path = Path::new("nested.ml");
4216        let mut analysis =
4217            analyze_ocaml_syntax(&FileContext { path, module: &[] }, source).unwrap();
4218        let span = analysis.functions[0]
4219            .calls
4220            .iter()
4221            .find(|call| call.label.default == "inner value")
4222            .unwrap()
4223            .span
4224            .clone();
4225        apply_typed_events(
4226            &mut analysis,
4227            &[TypedCallEvent {
4228                target: Some("Inner.target".to_owned()),
4229                signature: None,
4230                span,
4231            }],
4232        );
4233
4234        assert_eq!(
4235            analysis.functions[0]
4236                .calls
4237                .iter()
4238                .filter(|call| matches!(call.target, CallTarget::Direct(_)))
4239                .count(),
4240            1
4241        );
4242    }
4243
4244    #[test]
4245    fn extracts_functions_declared_inside_a_functor_body() {
4246        let source = r#"
4247            module Make (Store : sig val save : int -> unit end) = struct
4248              let run value = Store.save value
4249            end
4250        "#;
4251        let analysis = OcamlFrontend
4252            .analyze_file(
4253                &FileContext {
4254                    path: Path::new("functor.ml"),
4255                    module: &[],
4256                },
4257                source,
4258            )
4259            .unwrap();
4260        assert!(
4261            analysis
4262                .functions
4263                .iter()
4264                .any(|function| { function.id.module == ["Make"] && function.id.name == "run" })
4265        );
4266    }
4267}