Skip to main content

devscout_rs/semantic/
precedence.rs

1// Same-context authority: given what a same-context compiler fact confirmed
2// for one reference and whatever the ladder itself already pushed for that
3// same reference, decides whether the compiler's answer overrides it, and
4// keeps the running counters an enriched-lane audit reads back (confirmed
5// overrides, and how many of them actually displaced a different ladder
6// target -- the disagreement diagnostic). Never touches `edges` itself; the
7// caller (the resolver's own per-reference wrapper) owns truncation and the
8// one replacement push, so this module stays a pure decision plus a counter.
9
10use std::collections::HashSet;
11
12use crate::graph::{
13    Edge, EdgesByKind, FragRef, HeuristicByTier, HeuristicTier, SemanticProvenance,
14};
15use crate::resolve::DefIndex;
16
17use super::discovered;
18use super::layer::{LookupOutcome, SemanticLayer, SemanticTarget};
19use crate::graph::SemanticStats;
20
21/// A same-context compiler fact that overrides whatever the ladder already
22/// produced for this reference. `displaced` names the ladder's own target
23/// when it differs from `target` -- the disagreement this consumer must
24/// preserve as a diagnostic rather than a graph edge.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Override {
27    /// The compiler's own same-context answer.
28    pub target: SemanticTarget,
29    /// The ladder's own target, when it differs from `target`.
30    pub displaced: Option<String>,
31}
32
33/// Decides confirm/override/fall-through for one reference. `ladder_pushed`
34/// is the slice of edges the (untouched) ladder already pushed for this same
35/// reference before this decision runs; `None` (fall-through) leaves them
36/// exactly as they are.
37pub fn decide(outcome: LookupOutcome, ladder_pushed: &[Edge]) -> Option<Override> {
38    let LookupOutcome::Confirmed(target) = outcome else {
39        return None;
40    };
41    let displaced = ladder_pushed.iter().find_map(|e| match e {
42        Edge::UsesMember { to, .. } if *to != target.to_def_id => Some(to.clone()),
43        _ => None,
44    });
45    Some(Override { target, displaced })
46}
47
48/// The running counters one `map` run accumulates across every reference:
49/// how many same-context overrides fired, and how many of them displaced a
50/// different ladder target. Folded into `graph::SemanticStats` by `finish`,
51/// never into a graph edge.
52#[derive(Debug, Clone, Copy, Default)]
53pub struct SemanticDiagnostics {
54    /// Same-context overrides that fired.
55    pub confirmed: usize,
56    /// How many of those displaced a different ladder target.
57    pub disagreements: usize,
58}
59
60impl SemanticDiagnostics {
61    /// Records one override: always a confirmation, and a disagreement too
62    /// when it displaced a different ladder target.
63    pub fn record(&mut self, ovr: &Override) {
64        self.confirmed += 1;
65        if ovr.displaced.is_some() {
66            self.disagreements += 1;
67        }
68    }
69}
70
71/// Registers `r` at its own `(file, line, member)` key, when the layer is
72/// loaded and `r` names a member -- the extractor's own attempt at this
73/// site, regardless of whether it resolved, so `discovered.rs`'s later
74/// projection can tell "the extractor tried and missed" apart from "the
75/// extractor never looked here at all".
76pub fn track_reference(
77    layer: Option<&SemanticLayer>,
78    sites: &mut HashSet<(String, usize, String)>,
79    file: &str,
80    r: &FragRef,
81) {
82    if layer.is_some() {
83        if let Some(member) = &r.member {
84            sites.insert((file.to_string(), r.line, member.clone()));
85        }
86    }
87}
88
89/// The resolver's own per-reference wrapper: after the untouched ladder has
90/// run and pushed whatever it would have for `r` (the edges at
91/// `edges[pre_len..]`), consults the layer and, on a same-context
92/// confirmation, undoes exactly those pushes (edges and their own
93/// `edges_by_kind`/heuristic bookkeeping) and replaces them with the one
94/// compiler-vouched edge instead. A miss, or no layer at all, is a no-op: the
95/// ladder's own edges stand exactly as pushed.
96#[allow(
97    clippy::too_many_arguments,
98    reason = "each parameter is one of the resolver's own ledger fields, individually mutable, and the design forbids a post-pass over the finished edge vector that could fold them into one struct"
99)]
100pub fn apply(
101    layer: Option<&SemanticLayer>,
102    index: &DefIndex,
103    edges: &mut Vec<Edge>,
104    edges_by_kind: &mut EdgesByKind,
105    heuristic_edge_count: &mut usize,
106    heuristic_by_tier: &mut HeuristicByTier,
107    diag: &mut SemanticDiagnostics,
108    pre_len: usize,
109    file: &str,
110    r: &FragRef,
111) {
112    let (Some(layer), Some(member)) = (layer, r.member.as_deref()) else {
113        return;
114    };
115    let outcome = layer.lookup(index, file, r.line, member, r.arg_count);
116    let Some(ovr) = decide(outcome, &edges[pre_len..]) else {
117        return;
118    };
119    diag.record(&ovr);
120    // `edges_by_kind.uses_member` counts PRECISE uses-member edges only --
121    // a heuristic (ext/guess) push bumps `heuristic_edge_count`/
122    // `heuristic_by_tier` instead and never touches this counter at all
123    // (`resolve_graph_with_model`'s own module comment: "heuristic edges are
124    // counted here and nowhere else"). The truncated slice must therefore be
125    // walked to find how many of its own edges were PRECISE (tier `None`)
126    // before adjusting this counter -- subtracting the slice's raw length
127    // unconditionally underflows whenever an override truncates one or more
128    // heuristic edges, which a same-context fact confirming over an
129    // ambiguous scored-tier guess does routinely.
130    let mut precise_truncated = 0usize;
131    for e in &edges[pre_len..] {
132        match e {
133            Edge::UsesMember {
134                tier: Some(tier), ..
135            } => {
136                *heuristic_edge_count -= 1;
137                match tier {
138                    HeuristicTier::Ext => heuristic_by_tier.ext -= 1,
139                    HeuristicTier::Guess => heuristic_by_tier.guess -= 1,
140                }
141            }
142            Edge::UsesMember { tier: None, .. } => precise_truncated += 1,
143            _ => {}
144        }
145    }
146    edges_by_kind.uses_member -= precise_truncated;
147    edges.truncate(pre_len);
148    edges_by_kind.uses_member += 1;
149    edges.push(Edge::uses_member_semantic(
150        file.to_string(),
151        r.line,
152        ovr.target.to_def_id,
153        ovr.target.to_file,
154        Some(member.to_string()),
155        SemanticProvenance::Semantic,
156        ovr.target.overload_signature,
157    ));
158}
159
160/// The resolver's own end-of-run wrapper: projects every compiler-discovered
161/// site the main loop never referenced, then folds the run's own confirmed/
162/// disagreement counters together with the discovered count into one
163/// `SemanticStats`, or `None` when no layer loaded for this run at all.
164pub fn finish(
165    layer: Option<&SemanticLayer>,
166    index: &DefIndex,
167    sites: &HashSet<(String, usize, String)>,
168    edges: &mut Vec<Edge>,
169    diag: SemanticDiagnostics,
170) -> Option<SemanticStats> {
171    let layer = layer?;
172    let discovered = discovered::project(layer, index, sites, edges);
173    Some(SemanticStats {
174        confirmed: diag.confirmed,
175        disagreements: diag.disagreements,
176        discovered,
177    })
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::graph::HeuristicTier;
184
185    fn target(id: &str) -> SemanticTarget {
186        SemanticTarget {
187            to_def_id: id.to_string(),
188            to_file: "Target.cs".to_string(),
189            overload_signature: None,
190        }
191    }
192
193    #[test]
194    fn a_non_confirmed_outcome_never_overrides() {
195        assert!(decide(LookupOutcome::NoFact, &[]).is_none());
196    }
197
198    #[test]
199    fn confirming_the_same_target_the_ladder_already_bound_has_no_displaced_target() {
200        let pushed = vec![Edge::uses_member(
201            "Caller.cs".to_string(),
202            8,
203            "Ns.Widget".to_string(),
204            "Widget.cs".to_string(),
205            Some("Render".to_string()),
206            None,
207        )];
208        let ovr = decide(LookupOutcome::Confirmed(target("Ns.Widget")), &pushed).unwrap();
209        assert_eq!(ovr.displaced, None);
210    }
211
212    #[test]
213    fn confirming_a_different_target_records_the_displaced_one() {
214        let pushed = vec![Edge::uses_member(
215            "Caller.cs".to_string(),
216            8,
217            "Ns.WrongGuess".to_string(),
218            "WrongGuess.cs".to_string(),
219            Some("Render".to_string()),
220            Some(HeuristicTier::Guess),
221        )];
222        let ovr = decide(LookupOutcome::Confirmed(target("Ns.Widget")), &pushed).unwrap();
223        assert_eq!(ovr.displaced.as_deref(), Some("Ns.WrongGuess"));
224    }
225
226    #[test]
227    fn diagnostics_count_confirmations_and_disagreements_separately() {
228        let mut diag = SemanticDiagnostics::default();
229        diag.record(&Override {
230            target: target("Ns.Widget"),
231            displaced: None,
232        });
233        diag.record(&Override {
234            target: target("Ns.Widget"),
235            displaced: Some("Ns.WrongGuess".to_string()),
236        });
237        assert_eq!(diag.confirmed, 2);
238        assert_eq!(diag.disagreements, 1);
239    }
240}