sim_lib_lang_genconf/
property.rs1use 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
9pub 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
54pub 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}