Skip to main content

sim_codec/implementation/
runtime.rs

1//! The core decoder/encoder runtime contracts.
2//!
3//! Defines `Input`/`Output`, the `DecodePosition`/`DecodeTarget` output-position
4//! model, the `Decoder`/`Encoder` traits (with their located and tree variants),
5//! and the `CodecRuntime` glue that registers a codec as a runtime
6//! object.
7
8// conformance: codec runtime installs codec objects with position-aware decode.
9
10use std::sync::Arc;
11
12use sim_kernel::{
13    ClassRef, CodecId, Cx, LocatedExpr, LocatedExprTree, Object, Result, ShapeRef, Symbol, Value,
14    WriteCx,
15};
16
17use super::limits::ReadCx;
18
19/// Raw input handed to a [`Decoder`]: source text or source bytes.
20///
21/// Text codecs take [`Input::Text`]; binary codecs take [`Input::Bytes`]. A text
22/// codec given bytes interprets them as UTF-8 (see [`Input::into_string_for`]).
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum Input {
25    /// Source text.
26    Text(String),
27    /// Raw source bytes.
28    Bytes(Vec<u8>),
29}
30
31impl Input {
32    /// Take the input as UTF-8 text, decoding [`Input::Bytes`] and failing closed
33    /// with a codec error if the bytes are not valid UTF-8.
34    pub fn into_string(self) -> Result<String> {
35        self.into_string_for(CodecId(0))
36    }
37
38    /// Take the input as UTF-8 text, tagging invalid bytes with `codec`.
39    pub fn into_string_for(self, codec: CodecId) -> Result<String> {
40        match self {
41            Self::Text(text) => Ok(text),
42            Self::Bytes(bytes) => {
43                String::from_utf8(bytes).map_err(|err| sim_kernel::Error::CodecError {
44                    codec,
45                    message: format!("codec input is not valid UTF-8: {err}"),
46                })
47            }
48        }
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn input_utf8_error_uses_supplied_codec_id() {
58        let err = Input::Bytes(vec![0xff])
59            .into_string_for(CodecId(42))
60            .unwrap_err();
61
62        match err {
63            sim_kernel::Error::CodecError { codec, message } => {
64                assert_eq!(codec, CodecId(42));
65                assert!(message.contains("not valid UTF-8"), "{message}");
66            }
67            other => panic!("unexpected error {other:?}"),
68        }
69    }
70}
71
72/// Rendered output produced by an [`Encoder`]: text or bytes.
73///
74/// Text codecs emit [`Output::Text`]; binary codecs emit [`Output::Bytes`].
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub enum Output {
77    /// Rendered text.
78    Text(String),
79    /// Rendered bytes.
80    Bytes(Vec<u8>),
81}
82
83impl Output {
84    /// Take the output as UTF-8 text, decoding [`Output::Bytes`] and failing
85    /// closed with a codec error if the bytes are not valid UTF-8.
86    pub fn into_text(self) -> Result<String> {
87        match self {
88            Self::Text(text) => Ok(text),
89            Self::Bytes(bytes) => {
90                String::from_utf8(bytes).map_err(|err| sim_kernel::Error::CodecError {
91                    codec: CodecId(0),
92                    message: err.to_string(),
93                })
94            }
95        }
96    }
97}
98
99/// The syntactic position a decode is targeting, mirroring the kernel's
100/// `EncodePosition`.
101///
102/// Position is the core idea of the codec contract: a decoder reads the same
103/// text differently depending on where its result will land. A codec may, for
104/// example, lower forms to calls in [`DecodePosition::Eval`] but keep them as
105/// data everywhere else (see [`CodecDefaultDecode::target_for`]).
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum DecodePosition {
108    /// Decoding into evaluable position: the result will be evaluated.
109    Eval,
110    /// Decoding inside a quote: the result is literal structure, not evaluated.
111    Quote,
112    /// Decoding as plain data.
113    Data,
114    /// Decoding into a pattern (match/binding) position.
115    Pattern,
116}
117
118impl From<sim_kernel::EncodePosition> for DecodePosition {
119    fn from(position: sim_kernel::EncodePosition) -> Self {
120        match position {
121            sim_kernel::EncodePosition::Eval => Self::Eval,
122            sim_kernel::EncodePosition::Quote => Self::Quote,
123            sim_kernel::EncodePosition::Data => Self::Data,
124            sim_kernel::EncodePosition::Pattern => Self::Pattern,
125        }
126    }
127}
128
129/// The checked form a decode resolves to once a position is known: inert data
130/// or an evaluable term.
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub enum DecodeTarget {
133    /// Decode to a `Datum` (inert data).
134    Datum,
135    /// Decode to a `Term` (an evaluable form, e.g. a call).
136    Term,
137}
138
139/// A codec's policy for choosing a [`DecodeTarget`] from a [`DecodePosition`].
140///
141/// Data codecs always yield data; eval-aware codecs yield a term in eval
142/// position and data otherwise.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub enum CodecDefaultDecode {
145    /// Always decode to a `Datum`, regardless of position.
146    Datum,
147    /// Decode to a `Term` in [`DecodePosition::Eval`], otherwise to a `Datum`.
148    TermInEvalDatumOtherwise,
149}
150
151impl CodecDefaultDecode {
152    /// Resolve the [`DecodeTarget`] for `position` under this policy.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use sim_codec::{CodecDefaultDecode, DecodePosition, DecodeTarget};
158    ///
159    /// let policy = CodecDefaultDecode::TermInEvalDatumOtherwise;
160    /// assert_eq!(policy.target_for(DecodePosition::Eval), DecodeTarget::Term);
161    /// assert_eq!(policy.target_for(DecodePosition::Data), DecodeTarget::Datum);
162    ///
163    /// // A pure data codec always yields data.
164    /// assert_eq!(
165    ///     CodecDefaultDecode::Datum.target_for(DecodePosition::Eval),
166    ///     DecodeTarget::Datum,
167    /// );
168    /// ```
169    pub fn target_for(self, position: DecodePosition) -> DecodeTarget {
170        match (self, position) {
171            (Self::TermInEvalDatumOtherwise, DecodePosition::Eval) => DecodeTarget::Term,
172            _ => DecodeTarget::Datum,
173        }
174    }
175
176    /// The stable kebab-case name of this policy, for metadata and display.
177    pub fn as_symbol_name(self) -> &'static str {
178        match self {
179            Self::Datum => "datum",
180            Self::TermInEvalDatumOtherwise => "term-in-eval-datum-otherwise",
181        }
182    }
183}
184
185/// The core decode contract: turn [`Input`] into a checked kernel `Expr`.
186///
187/// Every codec that can read implements `Decoder`. The [`ReadCx`] carries the
188/// kernel context, the codec id, the read policy, and the decode limits applied
189/// to untrusted input.
190pub trait Decoder: Send + Sync {
191    /// Decode `input` into a kernel `Expr`, charging the [`ReadCx`] budget.
192    fn decode(&self, cx: &mut ReadCx<'_>, input: Input) -> Result<sim_kernel::Expr>;
193}
194
195/// A decoder that preserves source `Origin`, producing a [`LocatedExpr`].
196///
197/// Optional: [`CodecRuntime`] falls back to a plain [`Decoder`] with no origin
198/// when a codec provides none.
199pub trait LocatedDecoder: Send + Sync {
200    /// Decode `input` into a [`LocatedExpr`], attributing spans to `source_id`.
201    fn decode_located(
202        &self,
203        cx: &mut ReadCx<'_>,
204        input: Input,
205        source_id: String,
206    ) -> Result<LocatedExpr>;
207}
208
209/// A decoder that preserves the full source tree as a [`LocatedExprTree`],
210/// including trivia, for lossless round-tripping.
211pub trait TreeDecoder: Send + Sync {
212    /// Decode `input` into a [`LocatedExprTree`], attributing spans to
213    /// `source_id`.
214    fn decode_tree(
215        &self,
216        cx: &mut ReadCx<'_>,
217        input: Input,
218        source_id: String,
219    ) -> Result<LocatedExprTree>;
220}
221
222/// The core encode contract: render a kernel `Expr` to [`Output`].
223///
224/// Every codec that can write implements `Encoder`. The [`WriteCx`] carries the
225/// kernel context, the codec id, and the [`EncodeOptions`](sim_kernel::EncodeOptions)
226/// that fix the output position and fidelity.
227pub trait Encoder: Send + Sync {
228    /// Encode `expr` into [`Output`] under the context's encode options.
229    fn encode(&self, cx: &mut WriteCx<'_>, expr: &sim_kernel::Expr) -> Result<Output>;
230}
231
232/// An encoder that consumes a [`LocatedExpr`], able to use source origin for a
233/// higher-fidelity rendering than the plain [`Encoder`].
234pub trait LocatedEncoder: Send + Sync {
235    /// Encode a [`LocatedExpr`], optionally using its origin for fidelity.
236    fn encode_located(&self, cx: &mut WriteCx<'_>, expr: &LocatedExpr) -> Result<Output>;
237}
238
239/// An encoder that consumes a full [`LocatedExprTree`], able to reproduce trivia
240/// and exact layout for lossless round-trips.
241pub trait TreeEncoder: Send + Sync {
242    /// Encode a [`LocatedExprTree`], reproducing layout and trivia where present.
243    fn encode_tree(&self, cx: &mut WriteCx<'_>, expr: &LocatedExprTree) -> Result<Output>;
244}
245
246#[sim_citizen_derive::non_citizen(
247    reason = "codec runtime registry handle; reconstruct by loading the codec lib and using the codec symbol",
248    kind = "handle",
249    descriptor = "core/Codec"
250)]
251/// A registered codec as a runtime object: a symbol-named bundle of optional
252/// decode/encode capabilities plus the Shapes and default-decode policy it
253/// exposes.
254///
255/// This is the value the kernel hands back from a codec lookup. Each capability
256/// is optional; the dispatch methods ([`decode`](CodecRuntime::decode),
257/// [`encode_located`](CodecRuntime::encode_located), ...) pick the richest
258/// implementation present and fall back to the plain forms otherwise, failing
259/// closed with a codec error when none is provided.
260pub struct CodecRuntime {
261    /// Stable id of this codec.
262    pub id: CodecId,
263    /// The symbol the codec is registered and looked up under.
264    pub symbol: Symbol,
265    /// Plain `Expr` decoder, if the codec can read.
266    pub decoder: Option<Arc<dyn Decoder>>,
267    /// Origin-preserving decoder, if available.
268    pub located_decoder: Option<Arc<dyn LocatedDecoder>>,
269    /// Full-tree (lossless) decoder, if available.
270    pub tree_decoder: Option<Arc<dyn TreeDecoder>>,
271    /// Plain `Expr` encoder, if the codec can write.
272    pub encoder: Option<Arc<dyn Encoder>>,
273    /// Origin-aware encoder, if available.
274    pub located_encoder: Option<Arc<dyn LocatedEncoder>>,
275    /// Full-tree (lossless) encoder, if available.
276    pub tree_encoder: Option<Arc<dyn TreeEncoder>>,
277    /// Advisory browse metadata: a `Shape` that describes the expressions this
278    /// codec is intended for, surfaced in the codec's browse table so callers
279    /// can discover a codec's domain. It is descriptive only -- decode does not
280    /// validate results against it (each codec enforces its own grammar and
281    /// [`DecodeLimits`](crate::DecodeLimits) directly).
282    pub expr_shape: ShapeRef,
283    /// Shape describing this codec's options table.
284    pub options_shape: ShapeRef,
285    /// How this codec maps a [`DecodePosition`] to a [`DecodeTarget`].
286    pub default_decode: CodecDefaultDecode,
287}
288
289impl CodecRuntime {
290    /// Decode `input` with this codec's plain decoder, erroring if it has none.
291    pub fn decode(&self, cx: &mut ReadCx<'_>, input: Input) -> Result<sim_kernel::Expr> {
292        let Some(decoder) = &self.decoder else {
293            return Err(sim_kernel::Error::CodecError {
294                codec: self.id,
295                message: format!("codec {} has no decoder", self.symbol),
296            });
297        };
298        decoder.decode(cx, input)
299    }
300
301    /// Encode `expr` with this codec's plain encoder, erroring if it has none.
302    pub fn encode(&self, cx: &mut WriteCx<'_>, expr: &sim_kernel::Expr) -> Result<Output> {
303        let Some(encoder) = &self.encoder else {
304            return Err(sim_kernel::Error::CodecError {
305                codec: self.id,
306                message: format!("codec {} has no encoder", self.symbol),
307            });
308        };
309        encoder.encode(cx, expr)
310    }
311
312    /// Decode `input` preserving origin, falling back to [`decode`](Self::decode)
313    /// with no origin when the codec has no [`LocatedDecoder`].
314    pub fn decode_located(
315        &self,
316        cx: &mut ReadCx<'_>,
317        input: Input,
318        source_id: String,
319    ) -> Result<LocatedExpr> {
320        if let Some(decoder) = &self.located_decoder {
321            return decoder.decode_located(cx, input, source_id);
322        }
323        Ok(LocatedExpr {
324            expr: self.decode(cx, input)?,
325            origin: None,
326        })
327    }
328
329    /// Decode `input` into a full tree, falling back to
330    /// [`decode_located`](Self::decode_located) reconstructed recursively when
331    /// the codec has no [`TreeDecoder`].
332    pub fn decode_tree(
333        &self,
334        cx: &mut ReadCx<'_>,
335        input: Input,
336        source_id: String,
337    ) -> Result<LocatedExprTree> {
338        if let Some(decoder) = &self.tree_decoder {
339            return decoder.decode_tree(cx, input, source_id);
340        }
341        let located = self.decode_located(cx, input, source_id)?;
342        let mut tree = LocatedExprTree::from_expr_recursive(located.expr.clone());
343        tree.origin = located.origin;
344        Ok(tree)
345    }
346
347    /// Encode a [`LocatedExpr`], using the origin-aware encoder only when
348    /// lossless-origin output is requested; otherwise drop the origin and use
349    /// [`encode`](Self::encode).
350    pub fn encode_located(&self, cx: &mut WriteCx<'_>, expr: &LocatedExpr) -> Result<Output> {
351        if cx.options.lossless_origin
352            && let Some(encoder) = &self.located_encoder
353        {
354            return encoder.encode_located(cx, expr);
355        }
356        self.encode(cx, &expr.expr)
357    }
358
359    /// Encode a [`LocatedExprTree`], preferring the tree encoder then the
360    /// located encoder for lossless-origin output, and otherwise dropping to
361    /// [`encode`](Self::encode) on the bare expression.
362    pub fn encode_tree(&self, cx: &mut WriteCx<'_>, expr: &LocatedExprTree) -> Result<Output> {
363        if cx.options.lossless_origin {
364            if let Some(encoder) = &self.tree_encoder {
365                return encoder.encode_tree(cx, expr);
366            }
367            if let Some(encoder) = &self.located_encoder {
368                return encoder.encode_located(cx, &expr.located());
369            }
370        }
371        self.encode(cx, &expr.expr)
372    }
373}
374
375impl Object for CodecRuntime {
376    fn display(&self, _cx: &mut Cx) -> Result<String> {
377        Ok(format!("#<codec {}>", self.symbol))
378    }
379
380    fn as_any(&self) -> &dyn std::any::Any {
381        self
382    }
383}
384
385impl sim_kernel::ObjectCompat for CodecRuntime {
386    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
387        if let Some(value) = cx
388            .registry()
389            .class_by_symbol(&Symbol::qualified("core", "Codec"))
390        {
391            return Ok(value.clone());
392        }
393        cx.factory().class_stub(
394            sim_kernel::CORE_CODEC_CLASS_ID,
395            Symbol::qualified("core", "Codec"),
396        )
397    }
398    fn as_expr(&self, _cx: &mut Cx) -> Result<sim_kernel::Expr> {
399        Ok(sim_kernel::Expr::Symbol(self.symbol.clone()))
400    }
401    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
402        cx.factory().table(vec![
403            (
404                Symbol::new("symbol"),
405                cx.factory().string(self.symbol.to_string())?,
406            ),
407            (
408                Symbol::new("has-decoder"),
409                cx.factory().bool(self.decoder.is_some())?,
410            ),
411            (
412                Symbol::new("has-encoder"),
413                cx.factory().bool(self.encoder.is_some())?,
414            ),
415            (
416                Symbol::new("has-located-decoder"),
417                cx.factory().bool(self.located_decoder.is_some())?,
418            ),
419            (
420                Symbol::new("has-located-encoder"),
421                cx.factory().bool(self.located_encoder.is_some())?,
422            ),
423            (
424                Symbol::new("has-tree-decoder"),
425                cx.factory().bool(self.tree_decoder.is_some())?,
426            ),
427            (
428                Symbol::new("has-tree-encoder"),
429                cx.factory().bool(self.tree_encoder.is_some())?,
430            ),
431            (
432                Symbol::new("default-decode"),
433                cx.factory()
434                    .string(self.default_decode.as_symbol_name().to_owned())?,
435            ),
436            (Symbol::new("expr-shape"), self.expr_shape.clone()),
437            (Symbol::new("options-shape"), self.options_shape.clone()),
438        ])
439    }
440}