1use sim_kernel::{Cx, Diagnostic, Expr, ReadPolicy, Result, Symbol};
4use sim_shape::Shape;
5
6use crate::{DecodePosition, DecodedForm, Input, decode_default_with_codec};
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct GrammarCheck {
11 pub accepted: bool,
13 pub decoded: Option<DecodedForm>,
15 pub diagnostics: Vec<Diagnostic>,
17}
18
19pub fn grammar_check(
25 cx: &mut Cx,
26 shape: &dyn Shape,
27 codec: &Symbol,
28 text: &str,
29 position: DecodePosition,
30) -> Result<GrammarCheck> {
31 let decoded = match decode_default_with_codec(
32 cx,
33 codec,
34 Input::Text(text.to_owned()),
35 ReadPolicy::default(),
36 position,
37 ) {
38 Ok(decoded) => decoded,
39 Err(err) if is_decode_report(&err) => {
40 return Ok(GrammarCheck {
41 accepted: false,
42 decoded: None,
43 diagnostics: vec![Diagnostic::error(format!(
44 "decode with {codec} failed: {err}"
45 ))],
46 });
47 }
48 Err(err) => return Err(err),
49 };
50 let expr = decoded_expr(&decoded);
51 let matched = shape.check_expr(cx, &expr)?;
52 Ok(GrammarCheck {
53 accepted: matched.accepted,
54 decoded: Some(decoded),
55 diagnostics: matched.diagnostics,
56 })
57}
58
59fn decoded_expr(decoded: &DecodedForm) -> Expr {
60 match decoded {
61 DecodedForm::Datum(datum) => Expr::from(datum.clone()),
62 DecodedForm::Term(term) => Expr::from(term.clone()),
63 }
64}
65
66fn is_decode_report(error: &sim_kernel::Error) -> bool {
67 matches!(
68 error,
69 sim_kernel::Error::CodecError { .. }
70 | sim_kernel::Error::TypeMismatch { .. }
71 | sim_kernel::Error::Eval(_)
72 )
73}
74
75#[cfg(test)]
76mod tests {
77 use std::sync::Arc;
78
79 use sim_kernel::{
80 AbiVersion, CodecId, Cx, Datum, DefaultFactory, Dependency, Export, Expr, Factory, Lib,
81 LibManifest, LibTarget, Linker, LoadCx, MatchScore, Result, ShapeMatch, Symbol, Version,
82 testing::eager_cx as cx,
83 };
84
85 use crate::{CodecDefaultDecode, CodecRuntime, Decoder, Input, ReadCx, codec_value};
86
87 use super::*;
88
89 #[test]
90 fn grammar_check_accepts_decoded_shape_match() {
91 let mut cx = cx();
92 install_text_codec(&mut cx);
93 let check = grammar_check(
94 &mut cx,
95 &StringOnlyShape,
96 &codec_symbol(),
97 "ok",
98 DecodePosition::Data,
99 )
100 .unwrap();
101
102 assert!(check.accepted);
103 assert_eq!(
104 check.decoded,
105 Some(DecodedForm::Datum(Datum::String("ok".to_owned())))
106 );
107 assert!(check.diagnostics.is_empty());
108 }
109
110 #[test]
111 fn grammar_check_reports_decode_failure_without_decoded_form() {
112 let mut cx = cx();
113 install_text_codec(&mut cx);
114 let check = grammar_check(
115 &mut cx,
116 &StringOnlyShape,
117 &codec_symbol(),
118 "decode-error",
119 DecodePosition::Data,
120 )
121 .unwrap();
122
123 assert!(!check.accepted);
124 assert!(check.decoded.is_none());
125 assert!(
126 check
127 .diagnostics
128 .iter()
129 .any(|diagnostic| diagnostic.message.contains("decode with codec/test failed"))
130 );
131 }
132
133 #[test]
134 fn grammar_check_preserves_shape_diagnostics() {
135 let mut cx = cx();
136 install_text_codec(&mut cx);
137 let check = grammar_check(
138 &mut cx,
139 &StringOnlyShape,
140 &codec_symbol(),
141 "not-ok",
142 DecodePosition::Data,
143 )
144 .unwrap();
145
146 assert!(!check.accepted);
147 assert_eq!(
148 check.decoded,
149 Some(DecodedForm::Datum(Datum::String("not-ok".to_owned())))
150 );
151 assert_eq!(check.diagnostics.len(), 1);
152 assert_eq!(check.diagnostics[0].message, "expected ok");
153 }
154
155 fn install_text_codec(cx: &mut Cx) {
156 cx.load_lib(&TextCodecLib).unwrap();
157 }
158
159 fn codec_symbol() -> Symbol {
160 Symbol::qualified("codec", "test")
161 }
162
163 struct StringOnlyShape;
164
165 impl sim_kernel::Shape for StringOnlyShape {
166 fn check_expr(&self, _cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
167 match expr {
168 Expr::String(text) if text == "ok" => Ok(ShapeMatch::accept(MatchScore::exact(1))),
169 Expr::String(_) => Ok(ShapeMatch::reject("expected ok")),
170 _ => Ok(ShapeMatch::reject("expected string")),
171 }
172 }
173
174 fn check_value(&self, cx: &mut Cx, value: sim_kernel::Value) -> Result<ShapeMatch> {
175 let expr = value.object().as_expr(cx)?;
176 self.check_expr(cx, &expr)
177 }
178
179 fn describe(&self, _cx: &mut Cx) -> Result<sim_kernel::ShapeDoc> {
180 Ok(sim_kernel::ShapeDoc::new("ok string"))
181 }
182 }
183
184 struct TextDecoder;
185
186 impl Decoder for TextDecoder {
187 fn decode(&self, _cx: &mut ReadCx<'_>, input: Input) -> Result<Expr> {
188 let text = input.into_string()?;
189 if text == "decode-error" {
190 return Err(sim_kernel::Error::CodecError {
191 codec: CodecId(1),
192 message: "fixture decode failure".to_owned(),
193 });
194 }
195 Ok(Expr::String(text))
196 }
197 }
198
199 struct TextCodecLib;
200
201 impl Lib for TextCodecLib {
202 fn manifest(&self) -> LibManifest {
203 LibManifest {
204 id: codec_symbol(),
205 version: Version("0.1.0".to_owned()),
206 abi: AbiVersion { major: 0, minor: 1 },
207 target: LibTarget::HostRegistered,
208 requires: Vec::<Dependency>::new(),
209 capabilities: Vec::new(),
210 exports: vec![Export::Codec {
211 symbol: codec_symbol(),
212 codec_id: Some(CodecId(1)),
213 }],
214 }
215 }
216
217 fn load(&self, _cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
218 let nil = DefaultFactory.nil()?;
219 linker.codec_value(
220 codec_symbol(),
221 codec_value(CodecRuntime {
222 id: CodecId(1),
223 symbol: codec_symbol(),
224 decoder: Some(Arc::new(TextDecoder)),
225 located_decoder: None,
226 tree_decoder: None,
227 encoder: None,
228 located_encoder: None,
229 tree_encoder: None,
230 expr_shape: nil.clone(),
231 options_shape: nil,
232 default_decode: CodecDefaultDecode::Datum,
233 }),
234 )?;
235 Ok(())
236 }
237 }
238}