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}