Skip to main content

sim_codec/
grammar_check.rs

1//! Grammar-check reports over decoded codec text.
2
3use sim_kernel::{Cx, Diagnostic, Expr, ReadPolicy, Result, Symbol};
4use sim_shape::Shape;
5
6use crate::{DecodePosition, DecodedForm, Input, decode_default_with_codec};
7
8/// Report returned by [`grammar_check`].
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct GrammarCheck {
11    /// Whether decoding and Shape checking both accepted the text.
12    pub accepted: bool,
13    /// The decoded form, absent when the codec rejected the text.
14    pub decoded: Option<DecodedForm>,
15    /// Diagnostics from the decode or Shape check.
16    pub diagnostics: Vec<Diagnostic>,
17}
18
19/// Decode `text` with `codec`, then check the decoded syntax against `shape`.
20///
21/// Decode errors are reported as an unaccepted [`GrammarCheck`] instead of
22/// escaping as errors, so callers can feed the report into a repair loop. Runtime
23/// lookup errors and Shape implementation errors still return `Err`.
24pub 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, EagerPolicy, Export, Expr,
81        Factory, Lib, LibManifest, LibTarget, Linker, LoadCx, MatchScore, Result, ShapeMatch,
82        Symbol, Version,
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 cx() -> Cx {
156        Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory))
157    }
158
159    fn install_text_codec(cx: &mut Cx) {
160        cx.load_lib(&TextCodecLib).unwrap();
161    }
162
163    fn codec_symbol() -> Symbol {
164        Symbol::qualified("codec", "test")
165    }
166
167    struct StringOnlyShape;
168
169    impl sim_kernel::Shape for StringOnlyShape {
170        fn check_expr(&self, _cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
171            match expr {
172                Expr::String(text) if text == "ok" => Ok(ShapeMatch::accept(MatchScore::exact(1))),
173                Expr::String(_) => Ok(ShapeMatch::reject("expected ok")),
174                _ => Ok(ShapeMatch::reject("expected string")),
175            }
176        }
177
178        fn check_value(&self, cx: &mut Cx, value: sim_kernel::Value) -> Result<ShapeMatch> {
179            let expr = value.object().as_expr(cx)?;
180            self.check_expr(cx, &expr)
181        }
182
183        fn describe(&self, _cx: &mut Cx) -> Result<sim_kernel::ShapeDoc> {
184            Ok(sim_kernel::ShapeDoc::new("ok string"))
185        }
186    }
187
188    struct TextDecoder;
189
190    impl Decoder for TextDecoder {
191        fn decode(&self, _cx: &mut ReadCx<'_>, input: Input) -> Result<Expr> {
192            let text = input.into_string()?;
193            if text == "decode-error" {
194                return Err(sim_kernel::Error::CodecError {
195                    codec: CodecId(1),
196                    message: "fixture decode failure".to_owned(),
197                });
198            }
199            Ok(Expr::String(text))
200        }
201    }
202
203    struct TextCodecLib;
204
205    impl Lib for TextCodecLib {
206        fn manifest(&self) -> LibManifest {
207            LibManifest {
208                id: codec_symbol(),
209                version: Version("0.1.0".to_owned()),
210                abi: AbiVersion { major: 0, minor: 1 },
211                target: LibTarget::HostRegistered,
212                requires: Vec::<Dependency>::new(),
213                capabilities: Vec::new(),
214                exports: vec![Export::Codec {
215                    symbol: codec_symbol(),
216                    codec_id: Some(CodecId(1)),
217                }],
218            }
219        }
220
221        fn load(&self, _cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
222            let nil = DefaultFactory.nil()?;
223            linker.codec_value(
224                codec_symbol(),
225                codec_value(CodecRuntime {
226                    id: CodecId(1),
227                    symbol: codec_symbol(),
228                    decoder: Some(Arc::new(TextDecoder)),
229                    located_decoder: None,
230                    tree_decoder: None,
231                    encoder: None,
232                    located_encoder: None,
233                    tree_encoder: None,
234                    expr_shape: nil.clone(),
235                    options_shape: nil,
236                    default_decode: CodecDefaultDecode::Datum,
237                }),
238            )?;
239            Ok(())
240        }
241    }
242}