use std::{
panic::Location,
sync::{
Mutex,
atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering},
},
};
use omena_syntax::ident::AuthoredPropertyTextV0;
use serde::Serialize;
use crate::{
CascadeDeclaration, CascadeLevel, CascadeOutcome, SpecificityExactnessV0,
axis_order::CascadeKeyAxisV0,
ranking::{InexactSpecificityAdjudicationV0, adjudicate_inexact_specificity_v0},
};
static CAPTURE_ACTIVE: AtomicBool = AtomicBool::new(false);
static CAPTURED_ROWS: Mutex<Vec<CascadeRankedSetLossCensusRowV0>> = Mutex::new(Vec::new());
static CAPTURE_STATE_RECOVERY_COUNT: AtomicUsize = AtomicUsize::new(0);
static MEASUREMENT_INVOCATION_COUNT: AtomicUsize = AtomicUsize::new(0);
static RANKED_SET_OUTCOME_COUNT: AtomicUsize = AtomicUsize::new(0);
static RECOVERED_DEFINITE_OUTCOME_COUNT: AtomicUsize = AtomicUsize::new(0);
static MULTI_CANDIDATE_INEXACT_RANKED_SET_COUNT: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum CascadeRankedSetFunctionV0 {
CascadeProperty,
CascadePropertyOpenWorld,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum CascadeAxisPrefixV0 {
Level,
LayerRank,
ScopeProximity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum CascadeRankedSetLossClassV0 {
RecoverableAxisDominant { axis: CascadeAxisPrefixV0 },
AxisWinnerInexact,
NoStrictAxisDominance,
SingleInexactCandidate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum CascadeRankedSetFinalOutcomeV0 {
RankedSet,
Definite,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CascadeRankedSetLossCandidateV0 {
pub declaration_id: String,
pub level: CascadeLevel,
pub layer_rank: i32,
pub scope_proximity: u32,
pub specificity_exactness: SpecificityExactnessV0,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CascadeRankedSetLossCensusRowV0 {
pub function: CascadeRankedSetFunctionV0,
pub invocation_site: &'static str,
pub source_path: String,
pub property: AuthoredPropertyTextV0,
pub declaration_ids: Vec<String>,
pub candidate_count: usize,
pub candidates: Vec<CascadeRankedSetLossCandidateV0>,
pub classification: CascadeRankedSetLossClassV0,
pub final_outcome: CascadeRankedSetFinalOutcomeV0,
pub definite_winner_declaration_id: Option<String>,
}
impl PartialEq for CascadeRankedSetLossCensusRowV0 {
fn eq(&self, other: &Self) -> bool {
self.function == other.function
&& self.invocation_site == other.invocation_site
&& self.source_path == other.source_path
&& self
.property
.to_property_name()
.same_as(&other.property.to_property_name())
&& self.declaration_ids == other.declaration_ids
&& self.candidate_count == other.candidate_count
&& self.candidates == other.candidates
&& self.classification == other.classification
&& self.final_outcome == other.final_outcome
&& self.definite_winner_declaration_id == other.definite_winner_declaration_id
}
}
impl Eq for CascadeRankedSetLossCensusRowV0 {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CascadeRankedSetLossCaptureV0 {
pub schema_version: &'static str,
pub product: &'static str,
pub capture_state_recovery_count: usize,
pub measurement_invocation_count: usize,
pub ranked_set_outcome_count: usize,
pub recovered_definite_outcome_count: usize,
pub multi_candidate_inexact_ranked_set_count: usize,
pub rows: Vec<CascadeRankedSetLossCensusRowV0>,
}
pub fn capture_cascade_ranked_set_losses<R>(
operation: impl FnOnce() -> R,
) -> Result<(R, CascadeRankedSetLossCaptureV0), &'static str> {
CAPTURE_ACTIVE
.compare_exchange(false, true, AtomicOrdering::AcqRel, AtomicOrdering::Acquire)
.map_err(|_| "cascade ranked-set loss capture is already active")?;
CAPTURE_STATE_RECOVERY_COUNT.store(0, AtomicOrdering::Release);
captured_rows().clear();
MEASUREMENT_INVOCATION_COUNT.store(0, AtomicOrdering::Release);
RANKED_SET_OUTCOME_COUNT.store(0, AtomicOrdering::Release);
RECOVERED_DEFINITE_OUTCOME_COUNT.store(0, AtomicOrdering::Release);
MULTI_CANDIDATE_INEXACT_RANKED_SET_COUNT.store(0, AtomicOrdering::Release);
let guard = CaptureGuard;
let result = operation();
let mut rows = std::mem::take(&mut *captured_rows());
rows.sort_by(|left, right| {
let left_property_key = left.property.to_property_name().canonical_key();
let right_property_key = right.property.to_property_name().canonical_key();
(
left.function,
left.invocation_site,
left.source_path.as_str(),
left_property_key,
left.declaration_ids.as_slice(),
)
.cmp(&(
right.function,
right.invocation_site,
right.source_path.as_str(),
right_property_key,
right.declaration_ids.as_slice(),
))
});
drop(guard);
Ok((
result,
CascadeRankedSetLossCaptureV0 {
schema_version: "0",
product: "omena-cascade.ranked-set-loss-capture",
capture_state_recovery_count: CAPTURE_STATE_RECOVERY_COUNT
.load(AtomicOrdering::Acquire),
measurement_invocation_count: MEASUREMENT_INVOCATION_COUNT
.load(AtomicOrdering::Acquire),
ranked_set_outcome_count: RANKED_SET_OUTCOME_COUNT.load(AtomicOrdering::Acquire),
recovered_definite_outcome_count: RECOVERED_DEFINITE_OUTCOME_COUNT
.load(AtomicOrdering::Acquire),
multi_candidate_inexact_ranked_set_count: MULTI_CANDIDATE_INEXACT_RANKED_SET_COUNT
.load(AtomicOrdering::Acquire),
rows,
},
))
}
pub fn classify_cascade_ranked_set_loss(
declarations: &[CascadeDeclaration],
) -> CascadeRankedSetLossClassV0 {
match adjudicate_inexact_specificity_v0(declarations) {
InexactSpecificityAdjudicationV0::Recoverable { deciding_axis, .. } => {
CascadeRankedSetLossClassV0::RecoverableAxisDominant {
axis: match deciding_axis {
CascadeKeyAxisV0::Level => CascadeAxisPrefixV0::Level,
CascadeKeyAxisV0::LayerRank => CascadeAxisPrefixV0::LayerRank,
CascadeKeyAxisV0::ScopeProximity
| CascadeKeyAxisV0::SpecificityIds
| CascadeKeyAxisV0::SpecificityClasses
| CascadeKeyAxisV0::SpecificityElements
| CascadeKeyAxisV0::SourceOrder => unreachable!(
"an exact winner cannot cross inexact specificity to recover on a later axis"
),
},
}
}
InexactSpecificityAdjudicationV0::AxisWinnerInexact => {
CascadeRankedSetLossClassV0::AxisWinnerInexact
}
InexactSpecificityAdjudicationV0::NoStrictAxisDominance => {
CascadeRankedSetLossClassV0::NoStrictAxisDominance
}
InexactSpecificityAdjudicationV0::SingleInexactCandidate => {
CascadeRankedSetLossClassV0::SingleInexactCandidate
}
}
}
pub(crate) fn observe_cascade_outcome(
function: CascadeRankedSetFunctionV0,
caller: &'static Location<'static>,
outcome: &CascadeOutcome,
) {
if !CAPTURE_ACTIVE.load(AtomicOrdering::Acquire) {
return;
}
MEASUREMENT_INVOCATION_COUNT.fetch_add(1, AtomicOrdering::AcqRel);
let (declarations, final_outcome, definite_winner_declaration_id) = match outcome {
CascadeOutcome::RankedSet(declarations) => {
RANKED_SET_OUTCOME_COUNT.fetch_add(1, AtomicOrdering::AcqRel);
(
declarations.clone(),
CascadeRankedSetFinalOutcomeV0::RankedSet,
None,
)
}
CascadeOutcome::Definite {
winner,
also_considered,
..
} => {
let mut declarations = Vec::with_capacity(also_considered.len().saturating_add(1));
declarations.push(winner.clone());
declarations.extend(also_considered.iter().cloned());
if !declarations.iter().any(|declaration| {
declaration.specificity_exactness == SpecificityExactnessV0::Inexact
}) {
return;
}
assert_definite_inexact_outcome_is_recoverable(&declarations);
RECOVERED_DEFINITE_OUTCOME_COUNT.fetch_add(1, AtomicOrdering::AcqRel);
(
declarations,
CascadeRankedSetFinalOutcomeV0::Definite,
Some(winner.id.clone()),
)
}
CascadeOutcome::Inherit | CascadeOutcome::Top => return,
};
if !declarations
.iter()
.any(|declaration| declaration.specificity_exactness == SpecificityExactnessV0::Inexact)
{
return;
}
if final_outcome == CascadeRankedSetFinalOutcomeV0::RankedSet && declarations.len() > 1 {
MULTI_CANDIDATE_INEXACT_RANKED_SET_COUNT.fetch_add(1, AtomicOrdering::AcqRel);
}
let row = CascadeRankedSetLossCensusRowV0 {
function,
invocation_site: invocation_site(caller.file()),
source_path: caller.file().to_string(),
property: declarations
.first()
.map(|declaration| declaration.property.clone())
.unwrap_or_else(|| AuthoredPropertyTextV0::new("")),
declaration_ids: declarations
.iter()
.map(|declaration| declaration.id.clone())
.collect(),
candidate_count: declarations.len(),
candidates: declarations
.iter()
.map(|declaration| CascadeRankedSetLossCandidateV0 {
declaration_id: declaration.id.clone(),
level: declaration.key.level,
layer_rank: declaration.key.layer_rank.get(),
scope_proximity: declaration.key.scope_proximity,
specificity_exactness: declaration.specificity_exactness,
})
.collect(),
classification: classify_cascade_ranked_set_loss(&declarations),
final_outcome,
definite_winner_declaration_id,
};
captured_rows().push(row);
}
fn assert_definite_inexact_outcome_is_recoverable(declarations: &[CascadeDeclaration]) {
assert!(
matches!(
classify_cascade_ranked_set_loss(declarations),
CascadeRankedSetLossClassV0::RecoverableAxisDominant { .. }
),
"a definite inexact outcome must be justified by an earlier exact axis"
);
}
fn invocation_site(source_path: &str) -> &'static str {
if source_path.ends_with("omena-query/src/style/cascade_checker/runtime_state.rs") {
"queryRuntimeStateScenarioEvaluation"
} else if source_path.ends_with("omena-query/src/style/cascade_checker/confidence.rs") {
"queryCascadeMarginForEvaluation"
} else if source_path.ends_with("omena-query/src/style/cascade_checker/replica_ensemble.rs") {
"collectQueryReplicaEnsembleSiteOutcomes"
} else if source_path.ends_with("omena-cascade/src/computed_value.rs") {
"computeCascadeComputedValue"
} else if source_path.ends_with("omena-transform-passes/src/runtime/winner_equality.rs") {
"transformWinnerEqualityFromCascadeOutcome"
} else {
"unclassified"
}
}
fn captured_rows() -> std::sync::MutexGuard<'static, Vec<CascadeRankedSetLossCensusRowV0>> {
let (rows, recovered) = recover_captured_rows(CAPTURED_ROWS.lock(), &CAPTURED_ROWS);
if recovered {
CAPTURE_STATE_RECOVERY_COUNT.fetch_add(1, AtomicOrdering::AcqRel);
}
rows
}
fn recover_captured_rows<'a>(
lock: std::sync::LockResult<std::sync::MutexGuard<'a, Vec<CascadeRankedSetLossCensusRowV0>>>,
mutex: &'a Mutex<Vec<CascadeRankedSetLossCensusRowV0>>,
) -> (
std::sync::MutexGuard<'a, Vec<CascadeRankedSetLossCensusRowV0>>,
bool,
) {
match lock {
Ok(rows) => (rows, false),
Err(poisoned) => {
mutex.clear_poison();
let mut rows = poisoned.into_inner();
rows.clear();
(rows, true)
}
}
}
struct CaptureGuard;
impl Drop for CaptureGuard {
fn drop(&mut self) {
CAPTURE_ACTIVE.store(false, AtomicOrdering::Release);
}
}
#[cfg(test)]
mod tests {
use super::{
CascadeAxisPrefixV0, CascadeRankedSetFinalOutcomeV0, CascadeRankedSetLossCensusRowV0,
CascadeRankedSetLossClassV0, assert_definite_inexact_outcome_is_recoverable,
capture_cascade_ranked_set_losses, classify_cascade_ranked_set_loss, recover_captured_rows,
};
use crate::{
CascadeDeclaration, CascadeKey, CascadeLevel, CascadeOutcome, CascadeValue, LayerOrdinal,
OpenWorldTieEvidence, Specificity, SpecificityExactnessV0, cascade_property,
normalized_layer_rank,
};
fn declaration(
id: &str,
level: CascadeLevel,
layer_ordinal: i32,
scope_proximity: u32,
specificity: Specificity,
exactness: SpecificityExactnessV0,
) -> CascadeDeclaration {
CascadeDeclaration {
id: id.to_string(),
property: omena_syntax::ident::AuthoredPropertyTextV0::new("color"),
property_key: omena_syntax::ident::PropertyNameV0::standard("color").canonical_key(),
value: CascadeValue::Literal(id.to_string()),
key: CascadeKey::new(
level,
normalized_layer_rank(false, LayerOrdinal::new(layer_ordinal)),
scope_proximity,
specificity,
0,
),
open_world_tie_evidence: OpenWorldTieEvidence::NONE,
specificity_exactness: exactness,
}
}
#[test]
fn axis_winner_exactness_changes_the_recoverability_class() {
let lower = declaration(
"lower",
CascadeLevel::UserNormal,
0,
0,
Specificity::new(9, 9, 9),
SpecificityExactnessV0::Inexact,
);
let exact_winner = declaration(
"winner",
CascadeLevel::AuthorNormal,
0,
0,
Specificity::ZERO,
SpecificityExactnessV0::Exact,
);
assert_eq!(
classify_cascade_ranked_set_loss(&[lower.clone(), exact_winner.clone()]),
CascadeRankedSetLossClassV0::RecoverableAxisDominant {
axis: CascadeAxisPrefixV0::Level
}
);
let mut inexact_winner = exact_winner;
inexact_winner.specificity_exactness = SpecificityExactnessV0::Inexact;
assert_eq!(
classify_cascade_ranked_set_loss(&[lower, inexact_winner]),
CascadeRankedSetLossClassV0::AxisWinnerInexact
);
}
#[test]
fn specificity_only_winner_has_no_strict_axis_dominance() {
let weaker = declaration(
"weaker",
CascadeLevel::AuthorNormal,
0,
0,
Specificity::new(0, 1, 0),
SpecificityExactnessV0::Inexact,
);
let stronger = declaration(
"stronger",
CascadeLevel::AuthorNormal,
0,
0,
Specificity::new(1, 0, 0),
SpecificityExactnessV0::Exact,
);
assert_eq!(
classify_cascade_ranked_set_loss(&[weaker, stronger]),
CascadeRankedSetLossClassV0::NoStrictAxisDominance
);
}
#[test]
fn single_inexact_candidate_is_not_vacuously_recoverable() {
let candidate = declaration(
"only",
CascadeLevel::AuthorNormal,
0,
0,
Specificity::ZERO,
SpecificityExactnessV0::Inexact,
);
assert_eq!(
classify_cascade_ranked_set_loss(&[candidate]),
CascadeRankedSetLossClassV0::SingleInexactCandidate
);
}
#[test]
#[should_panic(expected = "requires an inexact declaration")]
fn exact_only_input_is_outside_the_loss_classifier_domain() {
let candidate = declaration(
"exact",
CascadeLevel::AuthorNormal,
0,
0,
Specificity::ZERO,
SpecificityExactnessV0::Exact,
);
let _ = classify_cascade_ranked_set_loss(&[candidate]);
}
#[test]
fn capture_retains_the_row_that_product_ranking_recovers_to_definite() {
let inexact_lower = declaration(
"inexact-lower",
CascadeLevel::UserNormal,
0,
0,
Specificity::new(9, 9, 9),
SpecificityExactnessV0::Inexact,
);
let exact_winner = declaration(
"exact-winner",
CascadeLevel::AuthorNormal,
0,
0,
Specificity::ZERO,
SpecificityExactnessV0::Exact,
);
let captured = capture_cascade_ranked_set_losses(|| {
cascade_property([inexact_lower, exact_winner], "color")
});
assert!(captured.is_ok(), "capture should be exclusive");
let Ok((outcome, capture)) = captured else {
return;
};
assert!(matches!(outcome, CascadeOutcome::Definite { .. }));
assert_eq!(capture.ranked_set_outcome_count, 0);
assert_eq!(capture.recovered_definite_outcome_count, 1);
assert_eq!(capture.multi_candidate_inexact_ranked_set_count, 0);
assert_eq!(capture.rows.len(), 1);
assert_eq!(
capture.rows[0].classification,
CascadeRankedSetLossClassV0::RecoverableAxisDominant {
axis: CascadeAxisPrefixV0::Level
}
);
assert_eq!(
capture.rows[0].final_outcome,
CascadeRankedSetFinalOutcomeV0::Definite
);
assert_eq!(
capture.rows[0].definite_winner_declaration_id.as_deref(),
Some("exact-winner")
);
}
#[test]
#[should_panic(expected = "a definite inexact outcome must be justified")]
fn release_build_rejects_an_unjustified_definite_outcome() {
let inexact = declaration(
"inexact",
CascadeLevel::AuthorNormal,
0,
0,
Specificity::new(0, 1, 0),
SpecificityExactnessV0::Inexact,
);
let exact = declaration(
"exact",
CascadeLevel::AuthorNormal,
0,
0,
Specificity::new(1, 0, 0),
SpecificityExactnessV0::Exact,
);
assert_definite_inexact_outcome_is_recoverable(&[exact, inexact]);
}
#[test]
fn poisoned_capture_storage_is_cleared_and_reported() {
let rows = std::sync::Arc::new(
std::sync::Mutex::<Vec<CascadeRankedSetLossCensusRowV0>>::new(Vec::new()),
);
let poisoned_rows = std::sync::Arc::clone(&rows);
let poison_result = std::thread::spawn(move || {
let _guard = match poisoned_rows.lock() {
Ok(guard) => guard,
Err(error) => error.into_inner(),
};
std::panic::resume_unwind(Box::new("poison capture storage"));
})
.join();
assert!(poison_result.is_err());
let (recovered_rows, recovered) = recover_captured_rows(rows.lock(), &rows);
assert!(recovered);
assert!(recovered_rows.is_empty());
assert!(!rows.is_poisoned());
}
}