Skip to main content

sim_lib_lang_genconf/
property.rs

1//! Generated expression round-trip properties.
2
3use sim_codec::{Input, decode_with_codec, encode_with_codec};
4use sim_kernel::{Cx, EncodeOptions, Expr, ReadPolicy, Symbol};
5use sim_lib_standard_core::{ExprRoundTripCase, ExprRoundTripObservation};
6
7use crate::space::ExprSpace;
8
9/// Checks the generated round-trip property for one expression and codec.
10///
11/// The property is the ROUNDTRIP_4 path: encode an expression through the codec,
12/// read the rendered source back with the same codec, and compare the two
13/// expression graphs with canonical equality.
14pub fn check_round_trip(cx: &mut Cx, codec: &Symbol, expr: &Expr) -> ExprRoundTripObservation {
15    let out = match encode_with_codec(cx, codec, expr, EncodeOptions::default()) {
16        Ok(out) => out,
17        Err(err) => {
18            return ExprRoundTripObservation::Diagnostic(Symbol::qualified(
19                "codec",
20                diagnostic_slug(&err),
21            ));
22        }
23    };
24    let source = match out.into_text() {
25        Ok(source) => source,
26        Err(err) => {
27            return ExprRoundTripObservation::Diagnostic(Symbol::qualified(
28                "codec",
29                diagnostic_slug(&err),
30            ));
31        }
32    };
33    let back = match decode_with_codec(cx, codec, Input::Text(source), ReadPolicy::default()) {
34        Ok(expr) => expr,
35        Err(err) => {
36            return ExprRoundTripObservation::Diagnostic(Symbol::qualified(
37                "codec",
38                diagnostic_slug(&err),
39            ));
40        }
41    };
42
43    let expected = expr_display(expr);
44    if expr.canonical_eq(&back) {
45        ExprRoundTripObservation::RoundTripped(expected)
46    } else {
47        ExprRoundTripObservation::Mismatch {
48            expected,
49            got: expr_display(&back),
50        }
51    }
52}
53
54/// Projects generated expressions into matrix expression round-trip cases.
55///
56/// Each case stores codec-rendered source text when the codec can encode the
57/// expression. Expressions outside that codec surface remain explicit generated
58/// cases with fallback source text, so later runners observe a diagnostic or gap
59/// instead of a silent pass. Generated cases never affect curated badges.
60pub fn generated_expr_cases(
61    cx: &mut Cx,
62    language: &Symbol,
63    codec: &Symbol,
64    space: &ExprSpace,
65    budget: usize,
66) -> Vec<ExprRoundTripCase> {
67    space
68        .enumerate(budget)
69        .into_iter()
70        .enumerate()
71        .map(|(index, expr)| ExprRoundTripCase {
72            symbol: Symbol::qualified(
73                format!("gen/{}", language.as_qualified_str()),
74                format!("expr-{index}"),
75            ),
76            language: language.clone(),
77            source: render_seed(cx, codec, &expr),
78            expected_display: Some(expr_display(&expr)),
79            affects_badge: None,
80        })
81        .collect()
82}
83
84fn render_seed(cx: &mut Cx, codec: &Symbol, expr: &Expr) -> String {
85    match encode_with_codec(cx, codec, expr, EncodeOptions::default()) {
86        Ok(out) => out.into_text().unwrap_or_else(|_| format!("{expr:?}")),
87        Err(_) => format!("{expr:?}"),
88    }
89}
90
91fn expr_display(expr: &Expr) -> String {
92    format!("Expr::{expr:?}")
93}
94
95fn diagnostic_slug(err: &sim_kernel::Error) -> &'static str {
96    let message = err.to_string().to_ascii_lowercase();
97    if message.contains("unsupported") {
98        "unsupported"
99    } else if message.contains("no encoder") {
100        "encode-unavailable"
101    } else {
102        "error"
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use sim_codec_lisp::LispCodecLib;
109    use sim_kernel::{DefaultFactory, EagerPolicy};
110    use sim_lib_lang_scheme::{SchemeCodecLib, scheme_reader_symbol};
111    use std::sync::Arc;
112
113    use super::*;
114
115    fn property_cx() -> Cx {
116        let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
117        sim_test_support::register_core_classes(&mut cx);
118        sim_test_support::register_f64_number_domain(&mut cx);
119        cx
120    }
121
122    fn register_lisp_codec(cx: &mut Cx) -> Symbol {
123        let codec = Symbol::qualified("codec", "lisp");
124        let lib = LispCodecLib::new(cx.registry_mut().fresh_codec_id()).unwrap();
125        cx.load_lib(&lib).unwrap();
126        codec
127    }
128
129    fn register_scheme_codec(cx: &mut Cx) -> Symbol {
130        let codec = scheme_reader_symbol();
131        let lib = SchemeCodecLib::new(cx.registry_mut().fresh_codec_id());
132        cx.load_lib(&lib).unwrap();
133        codec
134    }
135
136    #[test]
137    fn round_trip_pass_returns_round_tripped() {
138        let mut cx = property_cx();
139        let codec = register_lisp_codec(&mut cx);
140        let expr = Expr::Bool(true);
141
142        let observation = check_round_trip(&mut cx, &codec, &expr);
143
144        assert_eq!(
145            observation,
146            ExprRoundTripObservation::RoundTripped("Expr::Bool(true)".to_owned())
147        );
148    }
149
150    #[test]
151    fn round_trip_out_of_profile_expr_is_gap_or_diagnostic() {
152        let mut cx = property_cx();
153        let codec = register_scheme_codec(&mut cx);
154        let expr = Expr::Bool(true);
155
156        let observation = check_round_trip(&mut cx, &codec, &expr);
157
158        assert!(
159            matches!(
160                observation,
161                ExprRoundTripObservation::Gap(_) | ExprRoundTripObservation::Diagnostic(_)
162            ),
163            "out-of-profile expression silently passed: {observation:?}",
164        );
165    }
166
167    #[test]
168    fn generated_round_trip_cases_do_not_affect_badges() {
169        let mut cx = property_cx();
170        let codec = register_lisp_codec(&mut cx);
171        let language = Symbol::new("lisp");
172        let space = ExprSpace::r7rs_core_space(2);
173
174        let cases = generated_expr_cases(&mut cx, &language, &codec, &space, 4);
175
176        assert_eq!(cases.len(), 4);
177        for (index, case) in cases.iter().enumerate() {
178            assert_eq!(
179                case.symbol,
180                Symbol::qualified("gen/lisp", format!("expr-{index}"))
181            );
182            assert_eq!(case.language, language);
183            assert!(case.affects_badge.is_none());
184            assert!(
185                case.expected_display
186                    .as_deref()
187                    .is_some_and(|s| { s.starts_with("Expr::") })
188            );
189            assert!(!case.source.is_empty());
190        }
191    }
192}