use std::cmp::Ordering;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use super::evidence_descriptor::{EvidenceDescriptor, EvidenceKind, EvidenceTier};
use super::plan_table::SearchLaneKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SymbolOffsetRange {
pub start: usize,
pub end: usize,
}
impl SymbolOffsetRange {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CandidateResult {
pub path: PathBuf,
pub symbol_range: Option<SymbolOffsetRange>,
pub evidence: EvidenceDescriptor,
#[serde(skip_serializing_if = "Option::is_none")]
pub fusion_score: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lane_score: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub best_lane: Option<SearchLaneKind>,
}
impl CandidateResult {
pub fn new_exact(
path: PathBuf,
symbol_range: Option<SymbolOffsetRange>,
evidence: EvidenceDescriptor,
) -> Self {
assert_eq!(
evidence.tier,
EvidenceTier::Exact,
"exact candidate must carry EvidenceTier::Exact"
);
Self {
path,
symbol_range,
evidence,
fusion_score: None,
lane_score: None,
best_lane: Some(SearchLaneKind::Exact),
}
}
pub fn new_non_exact(
path: PathBuf,
symbol_range: Option<SymbolOffsetRange>,
evidence: EvidenceDescriptor,
fusion_score: f32,
lane_score: f32,
best_lane: SearchLaneKind,
) -> Self {
assert_eq!(
evidence.tier,
EvidenceTier::NonExact,
"non-exact candidate must carry EvidenceTier::NonExact"
);
Self {
path,
symbol_range,
evidence,
fusion_score: Some(fusion_score),
lane_score: Some(lane_score),
best_lane: Some(best_lane),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RankedTuple {
pub file: PathBuf,
pub symbol_range: Option<SymbolOffsetRange>,
pub r3_order_index: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub fusion_score: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lane_score: Option<f32>,
}
pub fn compare_fields_1_to_6(a: &CandidateResult, b: &CandidateResult) -> Ordering {
let a_def = a.evidence.kind == EvidenceKind::Definition;
let b_def = b.evidence.kind == EvidenceKind::Definition;
match b_def.cmp(&a_def) {
Ordering::Equal => {}
ord => return ord,
}
let a_e1 = a.evidence.kind == EvidenceKind::E1;
let b_e1 = b.evidence.kind == EvidenceKind::E1;
match b_e1.cmp(&a_e1) {
Ordering::Equal => {
if a_e1 && b_e1 {
match b.evidence.occurrences.cmp(&a.evidence.occurrences) {
Ordering::Equal => {}
ord => return ord,
}
}
}
ord => return ord,
}
let a_anchored = a.evidence.kind == EvidenceKind::Anchored;
let b_anchored = b.evidence.kind == EvidenceKind::Anchored;
match b_anchored.cmp(&a_anchored) {
Ordering::Equal => {
if a_anchored && b_anchored {
match b.evidence.matched_span.cmp(&a.evidence.matched_span) {
Ordering::Equal => {}
ord => return ord,
}
match a.evidence.gap_chars.cmp(&b.evidence.gap_chars) {
Ordering::Equal => {}
ord => return ord,
}
}
}
ord => return ord,
}
let a_e2 = a.evidence.kind == EvidenceKind::E2;
let b_e2 = b.evidence.kind == EvidenceKind::E2;
match b_e2.cmp(&a_e2) {
Ordering::Equal => {
if a_e2 && b_e2 {
match a.evidence.window_lines.cmp(&b.evidence.window_lines) {
Ordering::Equal => {}
ord => return ord,
}
}
}
ord => return ord,
}
match b.evidence.exact_form.cmp(&a.evidence.exact_form) {
Ordering::Equal => {}
ord => return ord,
}
a.evidence.generated.cmp(&b.evidence.generated)
}
pub fn compare_fields_9_to_11(a: &CandidateResult, b: &CandidateResult) -> Ordering {
match a
.path
.to_string_lossy()
.as_bytes()
.cmp(b.path.to_string_lossy().as_bytes())
{
Ordering::Equal => {}
ord => return ord,
}
match a.symbol_range.is_some().cmp(&b.symbol_range.is_some()) {
Ordering::Equal => {}
ord => return ord,
}
match (&a.symbol_range, &b.symbol_range) {
(Some(ra), Some(rb)) => match ra.start.cmp(&rb.start) {
Ordering::Equal => ra.end.cmp(&rb.end),
ord => ord,
},
_ => Ordering::Equal,
}
}
pub fn score_free_r3_cmp(a: &CandidateResult, b: &CandidateResult) -> Ordering {
match compare_fields_1_to_6(a, b) {
Ordering::Equal => compare_fields_9_to_11(a, b),
ord => ord,
}
}
pub fn r3_cmp(a: &CandidateResult, b: &CandidateResult) -> Ordering {
match compare_fields_1_to_6(a, b) {
Ordering::Equal => {}
ord => return ord,
}
let a_is_exact = a.evidence.tier == EvidenceTier::Exact;
let b_is_exact = b.evidence.tier == EvidenceTier::Exact;
if a_is_exact && b_is_exact {
return compare_fields_9_to_11(a, b);
}
if a_is_exact != b_is_exact {
panic!(
"exact-tier and non-exact candidates tied on fields (1)-(6): a={:?}, b={:?}",
a, b
);
}
match (a.fusion_score, b.fusion_score) {
(Some(fa), Some(fb)) => match fb.total_cmp(&fa) {
Ordering::Equal => {}
ord => return ord,
},
_ => {}
}
match (a.lane_score, b.lane_score) {
(Some(la), Some(lb)) => match lb.total_cmp(&la) {
Ordering::Equal => {
let a_order = a
.best_lane
.map(|l| l.default_plan_order_index())
.unwrap_or(usize::MAX);
let b_order = b
.best_lane
.map(|l| l.default_plan_order_index())
.unwrap_or(usize::MAX);
match a_order.cmp(&b_order) {
Ordering::Equal => {}
ord => return ord,
}
}
ord => return ord,
},
_ => {}
}
compare_fields_9_to_11(a, b)
}
pub fn sort_r3(results: &mut [CandidateResult]) {
results.sort_by(r3_cmp);
}
pub fn sort_score_free_r3(results: &mut [CandidateResult]) {
results.sort_by(score_free_r3_cmp);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_fixture_3a_run_char_total_ranks_24_char_first() {
let cand_a = CandidateResult {
path: PathBuf::from("a.rs"),
symbol_range: None,
evidence: EvidenceDescriptor::for_anchored(21, 5, true, false),
fusion_score: None,
lane_score: None,
best_lane: Some(SearchLaneKind::Exact),
};
let cand_b = CandidateResult {
path: PathBuf::from("b.rs"),
symbol_range: None,
evidence: EvidenceDescriptor::for_anchored(24, 5, true, false),
fusion_score: None,
lane_score: None,
best_lane: Some(SearchLaneKind::Exact),
};
assert_eq!(r3_cmp(&cand_b, &cand_a), Ordering::Less);
assert_eq!(r3_cmp(&cand_a, &cand_b), Ordering::Greater);
}
#[test]
fn exact_tier_is_score_free_r16() {
let cand_exact = CandidateResult::new_exact(
PathBuf::from("exact.rs"),
None,
EvidenceDescriptor::for_e1(3, true, false),
);
let json = serde_json::to_value(&cand_exact).unwrap();
assert!(
json.get("fusion_score").is_none(),
"fusion_score must be absent"
);
assert!(
json.get("lane_score").is_none(),
"lane_score must be absent"
);
}
#[test]
fn score_free_acyclicity_operational_assertion() {
let exact_cand = CandidateResult::new_exact(
PathBuf::from("exact.rs"),
None,
EvidenceDescriptor::for_e1(5, true, false),
);
let lexical_cand = CandidateResult {
path: PathBuf::from("lex.rs"),
symbol_range: None,
evidence: EvidenceDescriptor::for_non_exact(true, false),
fusion_score: None, lane_score: Some(0.85),
best_lane: Some(SearchLaneKind::Lexical),
};
let semantic_cand = CandidateResult {
path: PathBuf::from("sem.rs"),
symbol_range: None,
evidence: EvidenceDescriptor::for_non_exact(true, false),
fusion_score: None, lane_score: Some(0.92),
best_lane: Some(SearchLaneKind::Semantic),
};
let _stubbed_fusion_scorer = || -> f32 {
panic!("fusion scorer must not be called during lane canonical ordering or exact tier ordering");
};
let mut exact_list = vec![exact_cand.clone()];
sort_score_free_r3(&mut exact_list);
assert_eq!(exact_list.len(), 1);
let mut lex_list = vec![lexical_cand.clone()];
lex_list.sort_by(|a, b| {
b.lane_score
.unwrap()
.total_cmp(&a.lane_score.unwrap())
.then_with(|| score_free_r3_cmp(a, b))
});
assert_eq!(lex_list.len(), 1);
let mut sem_list = vec![semantic_cand.clone()];
sem_list.sort_by(|a, b| {
b.lane_score
.unwrap()
.total_cmp(&a.lane_score.unwrap())
.then_with(|| score_free_r3_cmp(a, b))
});
assert_eq!(sem_list.len(), 1);
assert_eq!(exact_list[0].path, PathBuf::from("exact.rs"));
}
#[test]
fn file_level_sorts_before_symbol_level_on_same_path() {
let file_level = CandidateResult::new_exact(
PathBuf::from("common.rs"),
None,
EvidenceDescriptor::for_definition(true, false),
);
let symbol_level = CandidateResult::new_exact(
PathBuf::from("common.rs"),
Some(SymbolOffsetRange::new(10, 50)),
EvidenceDescriptor::for_definition(true, false),
);
assert_eq!(r3_cmp(&file_level, &symbol_level), Ordering::Less);
assert_eq!(r3_cmp(&symbol_level, &file_level), Ordering::Greater);
}
}