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, 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}