use indexmap::IndexMap;
use sim_kernel::{Cx, Error, Expr, Result, Symbol, Value};
use crate::{
ConformanceOutcome, LanguageProfile, matrix_claims::publish_matrix_cell_claim,
standard_test_capability,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SourceExpectation {
LowersTo(String),
ExpectedGap {
code: Symbol,
reason: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SourceObservation {
LowersTo(String),
Gap {
code: Symbol,
reason: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SourceConformanceCase {
pub symbol: Symbol,
pub organ: Symbol,
pub source_name: String,
pub source: String,
pub expectation: SourceExpectation,
pub affects_badge: Option<Symbol>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExprRoundTripCase {
pub symbol: Symbol,
pub language: Symbol,
pub source: String,
pub expected_display: Option<String>,
pub affects_badge: Option<Symbol>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ExprRoundTripObservation {
RoundTripped(String),
Mismatch {
expected: String,
got: String,
},
Diagnostic(Symbol),
Gap(Symbol),
}
impl ExprRoundTripCase {
pub fn run_expr_round_trip(
&self,
cx: &mut Cx,
decode_fn: impl Fn(&mut Cx, &str) -> Result<Option<Expr>>,
) -> ExprRoundTripObservation {
match decode_fn(cx, &self.source) {
Err(err) => ExprRoundTripObservation::Diagnostic(Symbol::qualified(
"codec",
diagnostic_slug(&err),
)),
Ok(None) => ExprRoundTripObservation::Gap(Symbol::qualified("codec", "declared-gap")),
Ok(Some(expr)) => {
let got = expr_display(&expr);
match &self.expected_display {
None => ExprRoundTripObservation::RoundTripped(got),
Some(expected) if expected == &got => {
ExprRoundTripObservation::RoundTripped(got)
}
Some(expected) => ExprRoundTripObservation::Mismatch {
expected: expected.clone(),
got,
},
}
}
}
}
pub fn run(
&self,
cx: &mut Cx,
decode_fn: impl Fn(&mut Cx, &str) -> Result<Option<Expr>>,
) -> ExprRoundTripObservation {
self.run_expr_round_trip(cx, decode_fn)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LanguageRow {
pub language: Symbol,
pub profile: LanguageProfile,
pub cases: Vec<SourceConformanceCase>,
pub expr_cases: Vec<ExprRoundTripCase>,
}
impl LanguageRow {
pub fn declared_empty(language: Symbol, profile: LanguageProfile) -> Self {
Self {
language,
profile,
cases: Vec::new(),
expr_cases: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.cases.is_empty() && self.expr_cases.is_empty()
}
pub fn with_expr_cases(mut self, expr_cases: Vec<ExprRoundTripCase>) -> Self {
self.expr_cases = expr_cases;
self
}
}
#[derive(Clone, Debug)]
pub struct LanguageRowBuilder {
language: Symbol,
profile: LanguageProfile,
cases: Vec<SourceConformanceCase>,
expr_cases: Vec<ExprRoundTripCase>,
}
impl LanguageRowBuilder {
pub fn new(language: Symbol, profile: LanguageProfile) -> Self {
Self {
language,
profile,
cases: Vec::new(),
expr_cases: Vec::new(),
}
}
pub fn with_case(mut self, case: SourceConformanceCase) -> Self {
self.cases.push(case);
self
}
pub fn with_cases<I>(mut self, cases: I) -> Self
where
I: IntoIterator<Item = SourceConformanceCase>,
{
self.cases.extend(cases);
self
}
pub fn with_expr_cases<I>(mut self, cases: I) -> Self
where
I: IntoIterator<Item = ExprRoundTripCase>,
{
self.expr_cases.extend(cases);
self
}
pub fn build(self) -> LanguageRow {
LanguageRow {
language: self.language,
profile: self.profile,
cases: self.cases,
expr_cases: self.expr_cases,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MatrixCellResult {
pub language: Symbol,
pub profile: Symbol,
pub organ: Symbol,
pub case_symbol: Symbol,
pub outcome: ConformanceOutcome,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MatrixRunReport {
pub cells: Vec<MatrixCellResult>,
}
impl MatrixRunReport {
pub fn pass_count(&self) -> usize {
self.cells
.iter()
.filter(|cell| cell.outcome.is_pass())
.count()
}
pub fn gap_count(&self) -> usize {
self.cells
.iter()
.filter(|cell| cell.outcome.is_gap())
.count()
}
pub fn fail_count(&self) -> usize {
self.cells
.iter()
.filter(|cell| cell.outcome.is_fail())
.count()
}
pub fn language_fidelity(&self, language: &Symbol) -> Option<f32> {
let pass = self
.cells
.iter()
.filter(|cell| &cell.language == language && cell.outcome.is_pass())
.count();
let fail = self
.cells
.iter()
.filter(|cell| &cell.language == language && cell.outcome.is_fail())
.count();
if pass + fail == 0 {
None
} else {
Some(pass as f32 / (pass + fail) as f32)
}
}
pub fn conformance_card_fields(
&self,
cx: &mut Cx,
language: &Symbol,
) -> Result<Vec<(Symbol, Value)>> {
let pass = self.language_outcome_count(language, ConformanceOutcome::is_pass);
let gap = self.language_outcome_count(language, ConformanceOutcome::is_gap);
let fail = self.language_outcome_count(language, ConformanceOutcome::is_fail);
let fidelity = self
.language_fidelity(language)
.map(|value| format!("{:.0}%", value * 100.0))
.unwrap_or_else(|| "unscored".to_owned());
conformance_card_fields(cx, pass, gap, fail, fidelity)
}
pub fn unscored_conformance_card_fields(cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
conformance_card_fields(cx, 0, 0, 0, "unscored".to_owned())
}
pub fn publish_claims(&self, cx: &mut Cx) -> Result<()> {
cx.require(&standard_test_capability())?;
for cell in &self.cells {
publish_matrix_cell_claim(cx, cell)?;
}
Ok(())
}
fn language_outcome_count(
&self,
language: &Symbol,
matches: impl Fn(&ConformanceOutcome) -> bool,
) -> usize {
self.cells
.iter()
.filter(|cell| &cell.language == language && matches(&cell.outcome))
.count()
}
}
pub struct MatrixRunner;
impl MatrixRunner {
pub fn run_row<F>(cx: &mut Cx, row: &LanguageRow, run_case: F) -> MatrixRunReport
where
F: Fn(&mut Cx, &SourceConformanceCase) -> Result<SourceObservation>,
{
let mut cells = Vec::with_capacity(row.cases.len());
for case in &row.cases {
let outcome = match run_case(cx, case) {
Ok(observation) => compare_source_observation(case, observation),
Err(err) => ConformanceOutcome::fail_with(err.to_string()),
};
cells.push(MatrixCellResult {
language: row.language.clone(),
profile: row.profile.symbol.clone(),
organ: case.organ.clone(),
case_symbol: case.symbol.clone(),
outcome,
});
}
MatrixRunReport { cells }
}
}
pub fn compare_source_observation(
case: &SourceConformanceCase,
observation: SourceObservation,
) -> ConformanceOutcome {
match (&case.expectation, observation) {
(SourceExpectation::LowersTo(expected), SourceObservation::LowersTo(got)) => {
if expected == &got {
ConformanceOutcome::pass()
} else {
ConformanceOutcome::fail(format!("expected {expected}, got {got}"))
}
}
(
SourceExpectation::ExpectedGap { code, reason },
SourceObservation::Gap {
code: got,
reason: got_reason,
},
) => {
if code == &got {
ConformanceOutcome::gap(reason.clone())
} else {
ConformanceOutcome::fail(format!(
"expected gap {code}, got gap {got}: {got_reason}"
))
}
}
(SourceExpectation::ExpectedGap { code, .. }, SourceObservation::LowersTo(got)) => {
ConformanceOutcome::fail(format!("expected gap {code}, got {got}"))
}
(SourceExpectation::LowersTo(expected), SourceObservation::Gap { code, reason }) => {
ConformanceOutcome::fail(format!("expected {expected}, got gap {code}: {reason}"))
}
}
}
#[derive(Default)]
pub struct ConformanceMatrix {
rows: IndexMap<Symbol, LanguageRow>,
}
impl ConformanceMatrix {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, row: LanguageRow) {
let language = row.language.clone();
assert!(
self.rows.insert(language.clone(), row).is_none(),
"language already registered in matrix: {language}",
);
}
pub fn language_count(&self) -> usize {
self.rows.len()
}
pub fn row(&self, language: &Symbol) -> Option<&LanguageRow> {
self.rows.get(language)
}
pub fn iter_rows(&self) -> impl Iterator<Item = &LanguageRow> {
self.rows.values()
}
pub fn total_cases(&self) -> usize {
self.rows.values().map(|row| row.cases.len()).sum()
}
pub fn total_expr_cases(&self) -> usize {
self.rows.values().map(|row| row.expr_cases.len()).sum()
}
}
fn expr_display(expr: &Expr) -> String {
format!("Expr::{expr:?}")
}
fn diagnostic_slug(err: &Error) -> &'static str {
if err.to_string().to_ascii_lowercase().contains("unsupported") {
"unsupported"
} else {
"error"
}
}
fn conformance_card_fields(
cx: &mut Cx,
pass: usize,
gap: usize,
fail: usize,
fidelity: String,
) -> Result<Vec<(Symbol, Value)>> {
Ok(vec![
(conformance_field("pass"), count_value(cx, pass)?),
(conformance_field("gap"), count_value(cx, gap)?),
(conformance_field("fail"), count_value(cx, fail)?),
(
conformance_field("fidelity"),
cx.factory().string(fidelity)?,
),
])
}
fn conformance_field(name: &str) -> Symbol {
Symbol::new(format!("conformance.{name}"))
}
fn count_value(cx: &mut Cx, count: usize) -> Result<Value> {
cx.factory()
.number_literal(Symbol::qualified("numbers", "u64"), count.to_string())
}