1use indexmap::IndexMap;
4use sim_kernel::{Cx, Error, Expr, Result, Symbol, Value};
5
6use crate::{
7 ConformanceOutcome, LanguageProfile, matrix_claims::publish_matrix_cell_claim,
8 standard_test_capability,
9};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum SourceExpectation {
14 LowersTo(String),
16 ExpectedGap {
18 code: Symbol,
20 reason: String,
22 },
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum SourceObservation {
28 LowersTo(String),
30 Gap {
32 code: Symbol,
34 reason: String,
36 },
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum SourceConformanceCaseKind {
42 Observed,
44 DescriptorOnly,
46}
47
48impl SourceConformanceCaseKind {
49 fn cell_kind(self) -> MatrixCellKind {
50 match self {
51 Self::Observed => MatrixCellKind::SourceObserved,
52 Self::DescriptorOnly => MatrixCellKind::DescriptorOnly,
53 }
54 }
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct SourceConformanceCase {
60 pub symbol: Symbol,
62 pub organ: Symbol,
64 pub source_name: String,
66 pub source: String,
68 pub kind: SourceConformanceCaseKind,
70 pub expectation: SourceExpectation,
72 pub affects_badge: Option<Symbol>,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct ExprRoundTripCase {
83 pub symbol: Symbol,
85 pub language: Symbol,
87 pub source: String,
89 pub expected_display: Option<String>,
91 pub affects_badge: Option<Symbol>,
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
97pub enum ExprRoundTripObservation {
98 RoundTripped(String),
100 Mismatch {
102 expected: String,
104 got: String,
106 },
107 Diagnostic(Symbol),
109 Gap(Symbol),
111}
112
113impl ExprRoundTripCase {
114 pub fn run_expr_round_trip(
116 &self,
117 cx: &mut Cx,
118 decode_fn: impl Fn(&mut Cx, &str) -> Result<Option<Expr>>,
119 ) -> ExprRoundTripObservation {
120 match decode_fn(cx, &self.source) {
121 Err(err) => ExprRoundTripObservation::Diagnostic(Symbol::qualified(
122 "codec",
123 diagnostic_slug(&err),
124 )),
125 Ok(None) => ExprRoundTripObservation::Gap(Symbol::qualified("codec", "declared-gap")),
126 Ok(Some(expr)) => {
127 let got = expr_display(&expr);
128 match &self.expected_display {
129 None => ExprRoundTripObservation::RoundTripped(got),
130 Some(expected) if expected == &got => {
131 ExprRoundTripObservation::RoundTripped(got)
132 }
133 Some(expected) => ExprRoundTripObservation::Mismatch {
134 expected: expected.clone(),
135 got,
136 },
137 }
138 }
139 }
140 }
141
142 pub fn run(
144 &self,
145 cx: &mut Cx,
146 decode_fn: impl Fn(&mut Cx, &str) -> Result<Option<Expr>>,
147 ) -> ExprRoundTripObservation {
148 self.run_expr_round_trip(cx, decode_fn)
149 }
150}
151
152#[derive(Clone, Debug, PartialEq, Eq)]
159pub struct LanguageRow {
160 pub language: Symbol,
162 pub profile: LanguageProfile,
164 pub cases: Vec<SourceConformanceCase>,
166 pub expr_cases: Vec<ExprRoundTripCase>,
168}
169
170impl LanguageRow {
171 pub fn declared_empty(language: Symbol, profile: LanguageProfile) -> Self {
173 Self {
174 language,
175 profile,
176 cases: Vec::new(),
177 expr_cases: Vec::new(),
178 }
179 }
180
181 pub fn is_empty(&self) -> bool {
183 self.cases.is_empty() && self.expr_cases.is_empty()
184 }
185
186 pub fn with_expr_cases(mut self, expr_cases: Vec<ExprRoundTripCase>) -> Self {
188 self.expr_cases = expr_cases;
189 self
190 }
191}
192
193#[derive(Clone, Debug)]
195pub struct LanguageRowBuilder {
196 language: Symbol,
197 profile: LanguageProfile,
198 cases: Vec<SourceConformanceCase>,
199 expr_cases: Vec<ExprRoundTripCase>,
200}
201
202impl LanguageRowBuilder {
203 pub fn new(language: Symbol, profile: LanguageProfile) -> Self {
205 Self {
206 language,
207 profile,
208 cases: Vec::new(),
209 expr_cases: Vec::new(),
210 }
211 }
212
213 pub fn with_case(mut self, case: SourceConformanceCase) -> Self {
215 self.cases.push(case);
216 self
217 }
218
219 pub fn with_cases<I>(mut self, cases: I) -> Self
221 where
222 I: IntoIterator<Item = SourceConformanceCase>,
223 {
224 self.cases.extend(cases);
225 self
226 }
227
228 pub fn with_expr_cases<I>(mut self, cases: I) -> Self
230 where
231 I: IntoIterator<Item = ExprRoundTripCase>,
232 {
233 self.expr_cases.extend(cases);
234 self
235 }
236
237 pub fn build(self) -> LanguageRow {
239 LanguageRow {
240 language: self.language,
241 profile: self.profile,
242 cases: self.cases,
243 expr_cases: self.expr_cases,
244 }
245 }
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
250pub enum MatrixCellKind {
251 SourceObserved,
253 DescriptorOnly,
255 ExprRoundTrip,
257 GeneratedCoverage,
259}
260
261impl MatrixCellKind {
262 pub fn symbol(self) -> Symbol {
264 match self {
265 Self::SourceObserved => Symbol::qualified("standard-test", "source-observed"),
266 Self::DescriptorOnly => Symbol::qualified("standard-test", "descriptor-only"),
267 Self::ExprRoundTrip => Symbol::qualified("standard-test", "expr-round-trip"),
268 Self::GeneratedCoverage => Symbol::qualified("standard-test", "generated-coverage"),
269 }
270 }
271
272 fn is_scored(self) -> bool {
273 matches!(self, Self::SourceObserved | Self::ExprRoundTrip)
274 }
275}
276
277#[derive(Clone, Debug, PartialEq, Eq)]
279pub struct MatrixCellResult {
280 pub language: Symbol,
282 pub profile: Symbol,
284 pub organ: Symbol,
286 pub case_symbol: Symbol,
288 pub kind: MatrixCellKind,
290 pub affects_badge: Option<Symbol>,
292 pub outcome: ConformanceOutcome,
294}
295
296impl MatrixCellResult {
297 fn is_scored(&self) -> bool {
298 self.kind.is_scored()
299 }
300}
301
302#[derive(Clone, Debug, PartialEq, Eq)]
308pub struct MatrixRunReport {
309 pub cells: Vec<MatrixCellResult>,
311}
312
313impl MatrixRunReport {
314 pub fn pass_count(&self) -> usize {
316 self.cells
317 .iter()
318 .filter(|cell| cell.is_scored() && cell.outcome.is_pass())
319 .count()
320 }
321
322 pub fn gap_count(&self) -> usize {
324 self.cells
325 .iter()
326 .filter(|cell| cell.is_scored() && cell.outcome.is_gap())
327 .count()
328 }
329
330 pub fn fail_count(&self) -> usize {
332 self.cells
333 .iter()
334 .filter(|cell| cell.is_scored() && cell.outcome.is_fail())
335 .count()
336 }
337
338 pub fn language_fidelity(&self, language: &Symbol) -> Option<f32> {
341 let pass = self
342 .cells
343 .iter()
344 .filter(|cell| &cell.language == language && cell.is_scored() && cell.outcome.is_pass())
345 .count();
346 let fail = self
347 .cells
348 .iter()
349 .filter(|cell| &cell.language == language && cell.is_scored() && cell.outcome.is_fail())
350 .count();
351 if pass + fail == 0 {
352 None
353 } else {
354 Some(pass as f32 / (pass + fail) as f32)
355 }
356 }
357
358 pub fn conformance_card_fields(
363 &self,
364 cx: &mut Cx,
365 language: &Symbol,
366 ) -> Result<Vec<(Symbol, Value)>> {
367 let pass = self.language_outcome_count(language, ConformanceOutcome::is_pass);
368 let gap = self.language_outcome_count(language, ConformanceOutcome::is_gap);
369 let fail = self.language_outcome_count(language, ConformanceOutcome::is_fail);
370 let fidelity = self
371 .language_fidelity(language)
372 .map(|value| format!("{:.0}%", value * 100.0))
373 .unwrap_or_else(|| "unscored".to_owned());
374 conformance_card_fields(cx, pass, gap, fail, fidelity)
375 }
376
377 pub fn unscored_conformance_card_fields(cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
379 conformance_card_fields(cx, 0, 0, 0, "unscored".to_owned())
380 }
381
382 pub fn publish_claims(&self, cx: &mut Cx) -> Result<()> {
384 cx.require(&standard_test_capability())?;
385 for cell in &self.cells {
386 publish_matrix_cell_claim(cx, cell)?;
387 }
388 Ok(())
389 }
390
391 fn language_outcome_count(
392 &self,
393 language: &Symbol,
394 matches: impl Fn(&ConformanceOutcome) -> bool,
395 ) -> usize {
396 self.cells
397 .iter()
398 .filter(|cell| &cell.language == language && cell.is_scored() && matches(&cell.outcome))
399 .count()
400 }
401}
402
403pub struct MatrixRunner;
410
411impl MatrixRunner {
412 pub fn run_source_row<F>(cx: &mut Cx, row: &LanguageRow, run_case: F) -> MatrixRunReport
414 where
415 F: Fn(&mut Cx, &SourceConformanceCase) -> Result<SourceObservation>,
416 {
417 Self::run_row(cx, row, run_case, |_cx, _case| {
418 panic!("source-only row attempted to execute expression cases")
419 })
420 }
421
422 pub fn run_row<F, G>(
425 cx: &mut Cx,
426 row: &LanguageRow,
427 run_source_case: F,
428 run_expr_case: G,
429 ) -> MatrixRunReport
430 where
431 F: Fn(&mut Cx, &SourceConformanceCase) -> Result<SourceObservation>,
432 G: Fn(&mut Cx, &ExprRoundTripCase) -> Result<ExprRoundTripObservation>,
433 {
434 let mut cells = Vec::with_capacity(row.cases.len() + row.expr_cases.len());
435 for case in &row.cases {
436 let outcome = match run_source_case(cx, case) {
437 Ok(observation) => compare_source_observation(case, observation),
438 Err(err) => ConformanceOutcome::fail_with(err.to_string()),
439 };
440 cells.push(MatrixCellResult {
441 language: row.language.clone(),
442 profile: row.profile.symbol.clone(),
443 organ: case.organ.clone(),
444 case_symbol: case.symbol.clone(),
445 kind: case.kind.cell_kind(),
446 affects_badge: case.affects_badge.clone(),
447 outcome,
448 });
449 }
450 for case in &row.expr_cases {
451 let outcome = match run_expr_case(cx, case) {
452 Ok(observation) => compare_expr_observation(case, observation),
453 Err(err) => ConformanceOutcome::fail_with(err.to_string()),
454 };
455 cells.push(MatrixCellResult {
456 language: row.language.clone(),
457 profile: row.profile.symbol.clone(),
458 organ: expr_round_trip_organ(&row.language),
459 case_symbol: case.symbol.clone(),
460 kind: MatrixCellKind::ExprRoundTrip,
461 affects_badge: case.affects_badge.clone(),
462 outcome,
463 });
464 }
465 MatrixRunReport { cells }
466 }
467}
468
469pub fn compare_source_observation(
471 case: &SourceConformanceCase,
472 observation: SourceObservation,
473) -> ConformanceOutcome {
474 match (&case.expectation, observation) {
475 (SourceExpectation::LowersTo(expected), SourceObservation::LowersTo(got)) => {
476 if expected == &got {
477 ConformanceOutcome::pass()
478 } else {
479 ConformanceOutcome::fail(format!("expected {expected}, got {got}"))
480 }
481 }
482 (
483 SourceExpectation::ExpectedGap { code, reason },
484 SourceObservation::Gap {
485 code: got,
486 reason: got_reason,
487 },
488 ) => {
489 if code == &got {
490 ConformanceOutcome::gap(reason.clone())
491 } else {
492 ConformanceOutcome::fail(format!(
493 "expected gap {code}, got gap {got}: {got_reason}"
494 ))
495 }
496 }
497 (SourceExpectation::ExpectedGap { code, .. }, SourceObservation::LowersTo(got)) => {
498 ConformanceOutcome::fail(format!("expected gap {code}, got {got}"))
499 }
500 (SourceExpectation::LowersTo(expected), SourceObservation::Gap { code, reason }) => {
501 ConformanceOutcome::fail(format!("expected {expected}, got gap {code}: {reason}"))
502 }
503 }
504}
505
506pub fn compare_expr_observation(
508 case: &ExprRoundTripCase,
509 observation: ExprRoundTripObservation,
510) -> ConformanceOutcome {
511 match (&case.expected_display, observation) {
512 (Some(_), ExprRoundTripObservation::RoundTripped(_)) => ConformanceOutcome::pass(),
513 (Some(_), ExprRoundTripObservation::Mismatch { expected, got }) => {
514 ConformanceOutcome::fail(format!("expected {expected}, got {got}"))
515 }
516 (Some(expected), ExprRoundTripObservation::Diagnostic(code)) => {
517 ConformanceOutcome::fail(format!("expected {expected}, got diagnostic {code}"))
518 }
519 (Some(expected), ExprRoundTripObservation::Gap(code)) => {
520 ConformanceOutcome::fail(format!("expected {expected}, got gap {code}"))
521 }
522 (None, ExprRoundTripObservation::Gap(code)) => ConformanceOutcome::gap(code.to_string()),
523 (None, ExprRoundTripObservation::RoundTripped(got)) => {
524 ConformanceOutcome::fail(format!("expected declared gap, got {got}"))
525 }
526 (None, ExprRoundTripObservation::Diagnostic(code)) => {
527 ConformanceOutcome::fail(format!("expected declared gap, got diagnostic {code}"))
528 }
529 (None, ExprRoundTripObservation::Mismatch { expected, got }) => {
530 ConformanceOutcome::fail(format!("expected declared gap, got {expected} -> {got}"))
531 }
532 }
533}
534
535#[derive(Default)]
541pub struct ConformanceMatrix {
542 rows: IndexMap<Symbol, LanguageRow>,
543}
544
545impl ConformanceMatrix {
546 pub fn new() -> Self {
548 Self::default()
549 }
550
551 pub fn register(&mut self, row: LanguageRow) {
557 let language = row.language.clone();
558 assert!(
559 self.rows.insert(language.clone(), row).is_none(),
560 "language already registered in matrix: {language}",
561 );
562 }
563
564 pub fn language_count(&self) -> usize {
566 self.rows.len()
567 }
568
569 pub fn row(&self, language: &Symbol) -> Option<&LanguageRow> {
571 self.rows.get(language)
572 }
573
574 pub fn iter_rows(&self) -> impl Iterator<Item = &LanguageRow> {
576 self.rows.values()
577 }
578
579 pub fn total_cases(&self) -> usize {
581 self.rows.values().map(|row| row.cases.len()).sum()
582 }
583
584 pub fn total_expr_cases(&self) -> usize {
586 self.rows.values().map(|row| row.expr_cases.len()).sum()
587 }
588}
589
590fn expr_display(expr: &Expr) -> String {
591 format!("Expr::{expr:?}")
592}
593
594fn expr_round_trip_organ(language: &Symbol) -> Symbol {
595 Symbol::qualified(language.as_qualified_str(), "expr-round-trip")
596}
597
598fn diagnostic_slug(err: &Error) -> &'static str {
599 if err.to_string().to_ascii_lowercase().contains("unsupported") {
600 "unsupported"
601 } else {
602 "error"
603 }
604}
605
606fn conformance_card_fields(
607 cx: &mut Cx,
608 pass: usize,
609 gap: usize,
610 fail: usize,
611 fidelity: String,
612) -> Result<Vec<(Symbol, Value)>> {
613 Ok(vec![
614 (conformance_field("pass"), count_value(cx, pass)?),
615 (conformance_field("gap"), count_value(cx, gap)?),
616 (conformance_field("fail"), count_value(cx, fail)?),
617 (
618 conformance_field("fidelity"),
619 cx.factory().string(fidelity)?,
620 ),
621 ])
622}
623
624fn conformance_field(name: &str) -> Symbol {
625 Symbol::new(format!("conformance.{name}"))
626}
627
628fn count_value(cx: &mut Cx, count: usize) -> Result<Value> {
629 cx.factory()
630 .number_literal(Symbol::qualified("numbers", "u64"), count.to_string())
631}