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