Skip to main content

sim_codec_doc/
codec.rs

1//! The markup and `codec:doc` decoder/encoder host libs.
2//!
3//! Markup backends install as strict `codec:markup/<id>` runtime codecs.
4//! `codec:doc` remains as the compatibility document codec and chunk-function
5//! host lib.
6
7use std::sync::Arc;
8
9use sim_codec::{
10    CodecDefaultDecode, CodecRuntime, DecodeBudget, Decoder, Encoder, Input, Output, ReadCx,
11    codec_value,
12};
13use sim_kernel::{
14    AbiVersion, Cx, DefaultFactory, Dependency, Error, Export, Lib, LibManifest, LibTarget, Linker,
15    Result, Symbol, Version, WriteCx,
16};
17
18use crate::backend::{
19    BackendRegistry, MarkupBackend, MarkupDecodeOptions, MarkupEncodeOptions, MarkupError,
20    default_backend_registry,
21};
22use crate::document::DocValue;
23use crate::functions::{CATALOG_FUNCTIONS, CHUNK_FUNCTIONS, DocCatalogFunction, DocChunkFunction};
24use crate::markdown::MarkdownBackend;
25use crate::markup::MarkupDoc;
26
27/// Textual prefix for rendered markup codec symbols.
28pub const MARKUP_CODEC_PREFIX: &str = "codec/markup/";
29
30/// Return the runtime codec symbol for a markup backend id.
31pub fn markup_codec_symbol(id: &crate::BackendId) -> Symbol {
32    Symbol::qualified("codec", format!("markup/{}", id.as_str()))
33}
34
35/// Strict runtime codec for one markup backend.
36pub struct MarkupCodec {
37    backend: Arc<dyn MarkupBackend>,
38}
39
40impl MarkupCodec {
41    /// Create a codec backed by `backend`.
42    pub fn new(backend: Arc<dyn MarkupBackend>) -> Self {
43        Self { backend }
44    }
45
46    /// Return this codec's backend id.
47    pub fn backend_id(&self) -> crate::BackendId {
48        self.backend.id()
49    }
50}
51
52impl Decoder for MarkupCodec {
53    fn decode(&self, cx: &mut ReadCx<'_>, input: Input) -> Result<sim_kernel::Expr> {
54        let source = input.into_string_for(cx.codec)?;
55        let budget = DecodeBudget::new(cx.limits);
56        budget.check_input_bytes(cx.codec, source.len())?;
57        let (doc, _fidelity) = self
58            .backend
59            .decode(&source, &MarkupDecodeOptions::default())
60            .map_err(|err| err.into_kernel_error(cx.codec))?;
61        Ok(doc.as_expr())
62    }
63}
64
65impl Encoder for MarkupCodec {
66    fn encode(&self, cx: &mut WriteCx<'_>, expr: &sim_kernel::Expr) -> Result<Output> {
67        let doc = MarkupDoc::from_expr(expr).map_err(|err| {
68            MarkupError::InvalidDocument(err.to_string()).into_kernel_error(cx.codec)
69        })?;
70        let (source, fidelity) = self
71            .backend
72            .encode(&doc, &MarkupEncodeOptions::default())
73            .map_err(|err| err.into_kernel_error(cx.codec))?;
74        if !fidelity.dropped.is_empty() {
75            return Err(MarkupError::Encode(format!(
76                "backend {} reported {} dropped part(s)",
77                fidelity.backend,
78                fidelity.dropped.len()
79            ))
80            .into_kernel_error(cx.codec));
81        }
82        Ok(Output::Text(source))
83    }
84}
85
86/// The `codec:doc` decoder/encoder.
87///
88/// As a [`Decoder`] it turns document text into a markup document `Expr`; as an
89/// [`Encoder`] it writes a markup document, compatibility document value, or raw
90/// string back to text and fails closed on any other expression.
91pub struct DocCodec;
92
93impl Decoder for DocCodec {
94    fn decode(&self, cx: &mut ReadCx<'_>, input: Input) -> Result<sim_kernel::Expr> {
95        let source = input.into_string_for(cx.codec)?;
96        let budget = DecodeBudget::new(cx.limits);
97        budget.check_input_bytes(cx.codec, source.len())?;
98        let (doc, _fidelity) = MarkdownBackend
99            .decode(&source, &MarkupDecodeOptions::default())
100            .map_err(|err| err.into_kernel_error(cx.codec))?;
101        Ok(doc.as_expr())
102    }
103}
104
105impl Encoder for DocCodec {
106    fn encode(&self, cx: &mut WriteCx<'_>, expr: &sim_kernel::Expr) -> Result<Output> {
107        match expr {
108            sim_kernel::Expr::String(text) => Ok(Output::Text(text.clone())),
109            sim_kernel::Expr::Map(_) => Ok(Output::Text(encode_doc_expr(expr).map_err(|err| {
110                Error::CodecError {
111                    codec: cx.codec,
112                    message: err.to_string(),
113                }
114            })?)),
115            _ => Err(Error::CodecError {
116                codec: cx.codec,
117                message: "codec:doc encodes document values or strings".to_owned(),
118            }),
119        }
120    }
121}
122
123fn encode_doc_expr(expr: &sim_kernel::Expr) -> Result<String> {
124    match MarkupDoc::from_expr(expr) {
125        Ok(doc) => Ok(doc.to_source_text()),
126        Err(markup_error) => match DocValue::from_expr(expr) {
127            Ok(doc) => Ok(doc.text),
128            Err(_) => Err(markup_error),
129        },
130    }
131}
132
133struct RegisteredBackend {
134    backend: Arc<dyn MarkupBackend>,
135    codec_id: sim_kernel::CodecId,
136}
137
138struct MarkupCodecLib {
139    backends: Vec<RegisteredBackend>,
140}
141
142impl MarkupCodecLib {
143    fn new(cx: &mut Cx, registry: BackendRegistry) -> Self {
144        let backends = registry
145            .iter()
146            .map(|(_, backend)| RegisteredBackend {
147                backend: backend.clone(),
148                codec_id: cx.registry_mut().fresh_codec_id(),
149            })
150            .collect();
151        Self { backends }
152    }
153}
154
155impl Lib for MarkupCodecLib {
156    fn manifest(&self) -> LibManifest {
157        LibManifest {
158            id: Symbol::qualified("codec", "markup"),
159            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
160            abi: AbiVersion { major: 0, minor: 1 },
161            target: LibTarget::HostRegistered,
162            requires: Vec::<Dependency>::new(),
163            capabilities: Vec::new(),
164            exports: self
165                .backends
166                .iter()
167                .map(|registered| Export::Codec {
168                    symbol: markup_codec_symbol(&registered.backend.id()),
169                    codec_id: Some(registered.codec_id),
170                })
171                .collect(),
172        }
173    }
174
175    fn load(&self, _cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> Result<()> {
176        let expr_shape =
177            sim_codec::resolve_expr_shape(linker, &Symbol::qualified("codec", "Document"))?;
178        let options_shape = sim_codec::resolve_options_shape(linker)?;
179
180        for registered in &self.backends {
181            let symbol = markup_codec_symbol(&registered.backend.id());
182            linker.codec_value(
183                symbol.clone(),
184                codec_value(CodecRuntime {
185                    id: registered.codec_id,
186                    symbol,
187                    decoder: Some(Arc::new(MarkupCodec::new(registered.backend.clone()))),
188                    located_decoder: None,
189                    tree_decoder: None,
190                    encoder: Some(Arc::new(MarkupCodec::new(registered.backend.clone()))),
191                    located_encoder: None,
192                    tree_encoder: None,
193                    expr_shape: expr_shape.clone(),
194                    options_shape: options_shape.clone(),
195                    default_decode: CodecDefaultDecode::Datum,
196                }),
197            )?;
198        }
199        Ok(())
200    }
201}
202
203/// The host-registered [`Lib`] that installs [`DocCodec`] as `codec:doc` and
204/// the `doc/chunk-*` chunking functions.
205pub struct DocCodecLib {
206    symbol: Symbol,
207    codec_id: sim_kernel::CodecId,
208}
209
210impl DocCodecLib {
211    /// Create the lib bound to the given codec id (obtained from
212    /// [`Registry::fresh_codec_id`](sim_kernel::Registry::fresh_codec_id)).
213    pub fn new(id: sim_kernel::CodecId) -> Self {
214        Self {
215            symbol: Symbol::qualified("codec", "doc"),
216            codec_id: id,
217        }
218    }
219}
220
221impl Lib for DocCodecLib {
222    fn manifest(&self) -> LibManifest {
223        let mut exports = vec![Export::Codec {
224            symbol: self.symbol.clone(),
225            codec_id: Some(self.codec_id),
226        }];
227        exports.extend(
228            CHUNK_FUNCTIONS
229                .iter()
230                .map(|kind| Export::Function {
231                    symbol: kind.symbol(),
232                    function_id: None,
233                })
234                .chain(CATALOG_FUNCTIONS.iter().map(|kind| Export::Function {
235                    symbol: kind.symbol(),
236                    function_id: None,
237                })),
238        );
239        LibManifest {
240            id: self.symbol.clone(),
241            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
242            abi: AbiVersion { major: 0, minor: 1 },
243            target: LibTarget::HostRegistered,
244            requires: Vec::<Dependency>::new(),
245            capabilities: Vec::new(),
246            exports,
247        }
248    }
249
250    fn load(&self, cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> Result<()> {
251        let _factory = DefaultFactory;
252        let expr_shape =
253            sim_codec::resolve_expr_shape(linker, &Symbol::qualified("codec", "Document"))?;
254        let options_shape = sim_codec::resolve_options_shape(linker)?;
255
256        linker.codec_value(
257            self.symbol.clone(),
258            codec_value(CodecRuntime {
259                id: self.codec_id,
260                symbol: self.symbol.clone(),
261                decoder: Some(Arc::new(DocCodec)),
262                located_decoder: None,
263                tree_decoder: None,
264                encoder: Some(Arc::new(DocCodec)),
265                located_encoder: None,
266                tree_encoder: None,
267                expr_shape,
268                options_shape,
269                default_decode: CodecDefaultDecode::Datum,
270            }),
271        )?;
272
273        for kind in CHUNK_FUNCTIONS {
274            linker.function_value(
275                kind.symbol(),
276                cx.factory()
277                    .opaque(Arc::new(DocChunkFunction::new(*kind)))?,
278            )?;
279        }
280        for kind in CATALOG_FUNCTIONS {
281            linker.function_value(
282                kind.symbol(),
283                cx.factory()
284                    .opaque(Arc::new(DocCatalogFunction::new(*kind)))?,
285            )?;
286        }
287        Ok(())
288    }
289}
290
291/// Install [`DocCodecLib`] into `cx`, registering `codec:doc` and the
292/// `doc/chunk-*` functions with a freshly allocated codec id.
293pub fn install_doc_codec(cx: &mut Cx) -> Result<()> {
294    install_markup_codecs(cx, default_backend_registry())?;
295    let lib = DocCodecLib::new(cx.registry_mut().fresh_codec_id());
296    cx.load_lib(&lib)?;
297    Ok(())
298}
299
300/// Install each registered markup backend as `codec:markup/<id>`.
301pub fn install_markup_codecs(cx: &mut Cx, registry: BackendRegistry) -> Result<()> {
302    if registry.is_empty() {
303        return Err(Error::Lib("markup backend registry is empty".to_owned()));
304    }
305    let lib = MarkupCodecLib::new(cx, registry);
306    cx.load_lib(&lib)?;
307    Ok(())
308}