Skip to main content

sim_codec/implementation/
portable.rs

1//! Codec-neutral, lossless text form for the data subset of `Expr`.
2//!
3//! Domain codecs (`codec:scene`, `codec:intent`, ...) must round-trip arbitrary
4//! data values without borrowing a general codec's grammar or losing
5//! information. This module provides exactly that: a small self-delimiting
6//! textual form that round-trips the data subset of `Expr` exactly -- maps,
7//! lists, vectors, sets, and the atoms (nil, bool, number, symbol, string,
8//! bytes). Eval-only `Expr` forms (calls, infix/prefix/postfix, quotes, blocks,
9//! locals, annotations, extensions) are not data and are rejected by
10//! [`encode_portable`], which is how a domain codec fails closed.
11//!
12//! Tag bytes: `_` nil, `T`/`F` bool, `N` number, `S` symbol, `R` string,
13//! `B` bytes, `(` list, `[` vector, `{` map, `%(` set. Symbol payloads start
14//! with `Q` (qualified) or `U` (unqualified); quoted strings are delimited by
15//! `"` with `\\ \" \n \r \t` escapes.
16
17use sim_kernel::{CodecId, Error, Expr, NumberLiteral, Result, Symbol};
18use sim_lib_net_core::hex_encode;
19
20/// Serialize a data-subset `Expr` into the codec-neutral portable text form.
21///
22/// `codec` is used only to tag any error with the calling codec's id.
23///
24/// # Examples
25///
26/// ```
27/// use sim_codec::{decode_portable, encode_portable};
28/// use sim_kernel::{CodecId, Expr};
29///
30/// let expr = Expr::List(vec![Expr::Nil, Expr::Bool(true), Expr::String("hi".into())]);
31/// let text = encode_portable(CodecId(0), &expr).unwrap();
32/// assert_eq!(decode_portable(CodecId(0), &text).unwrap(), expr);
33///
34/// // Eval-only forms are not data and fail closed.
35/// assert!(encode_portable(CodecId(0), &Expr::Block(vec![])).is_err());
36/// ```
37pub fn encode_portable(codec: CodecId, expr: &Expr) -> Result<String> {
38    let mut out = String::new();
39    write_value(codec, expr, &mut out)?;
40    Ok(out)
41}
42
43/// Parse codec-neutral portable text back into an `Expr`, failing closed on any
44/// malformed input rather than panicking.
45pub fn decode_portable(codec: CodecId, source: &str) -> Result<Expr> {
46    let mut parser = Parser {
47        bytes: source.as_bytes(),
48        pos: 0,
49        codec,
50    };
51    let expr = parser.parse_value()?;
52    parser.skip_ws();
53    if parser.pos != parser.bytes.len() {
54        return Err(parser.error("trailing input after value"));
55    }
56    Ok(expr)
57}
58
59fn unsupported(codec: CodecId, form: &str) -> Error {
60    Error::CodecError {
61        codec,
62        message: format!("portable text cannot encode a non-data expression form: {form}"),
63    }
64}
65
66fn write_value(codec: CodecId, expr: &Expr, out: &mut String) -> Result<()> {
67    match expr {
68        Expr::Nil => out.push('_'),
69        Expr::Bool(true) => out.push('T'),
70        Expr::Bool(false) => out.push('F'),
71        Expr::Number(number) => {
72            out.push('N');
73            write_symbol_payload(&number.domain, out);
74            write_qstr(&number.canonical, out);
75        }
76        Expr::Symbol(symbol) => {
77            out.push('S');
78            write_symbol_payload(symbol, out);
79        }
80        Expr::String(text) => {
81            out.push('R');
82            write_qstr(text, out);
83        }
84        Expr::Bytes(bytes) => {
85            out.push('B');
86            write_qstr(&hex_encode(bytes), out);
87        }
88        Expr::List(items) => write_seq(codec, '(', ')', items, out)?,
89        Expr::Vector(items) => write_seq(codec, '[', ']', items, out)?,
90        Expr::Set(items) => {
91            out.push('%');
92            write_seq(codec, '(', ')', items, out)?;
93        }
94        Expr::Map(entries) => {
95            out.push('{');
96            for (key, value) in entries {
97                out.push(' ');
98                write_value(codec, key, out)?;
99                out.push(' ');
100                write_value(codec, value, out)?;
101            }
102            out.push_str(" }");
103        }
104        Expr::Local(_) => return Err(unsupported(codec, "local")),
105        Expr::Call { .. } => return Err(unsupported(codec, "call")),
106        Expr::Infix { .. } => return Err(unsupported(codec, "infix")),
107        Expr::Prefix { .. } => return Err(unsupported(codec, "prefix")),
108        Expr::Postfix { .. } => return Err(unsupported(codec, "postfix")),
109        Expr::Block(_) => return Err(unsupported(codec, "block")),
110        Expr::Quote { .. } => return Err(unsupported(codec, "quote")),
111        Expr::Annotated { .. } => return Err(unsupported(codec, "annotated")),
112        Expr::Extension { .. } => return Err(unsupported(codec, "extension")),
113    }
114    Ok(())
115}
116
117fn write_seq(
118    codec: CodecId,
119    open: char,
120    close: char,
121    items: &[Expr],
122    out: &mut String,
123) -> Result<()> {
124    out.push(open);
125    for item in items {
126        out.push(' ');
127        write_value(codec, item, out)?;
128    }
129    out.push(' ');
130    out.push(close);
131    Ok(())
132}
133
134fn write_symbol_payload(symbol: &Symbol, out: &mut String) {
135    match &symbol.namespace {
136        Some(namespace) => {
137            out.push('Q');
138            write_qstr(namespace, out);
139            write_qstr(&symbol.name, out);
140        }
141        None => {
142            out.push('U');
143            write_qstr(&symbol.name, out);
144        }
145    }
146}
147
148fn write_qstr(text: &str, out: &mut String) {
149    out.push('"');
150    for ch in text.chars() {
151        match ch {
152            '\\' => out.push_str("\\\\"),
153            '"' => out.push_str("\\\""),
154            '\n' => out.push_str("\\n"),
155            '\r' => out.push_str("\\r"),
156            '\t' => out.push_str("\\t"),
157            other => out.push(other),
158        }
159    }
160    out.push('"');
161}
162
163struct Parser<'a> {
164    bytes: &'a [u8],
165    pos: usize,
166    codec: CodecId,
167}
168
169impl Parser<'_> {
170    fn error(&self, message: impl Into<String>) -> Error {
171        Error::CodecError {
172            codec: self.codec,
173            message: format!(
174                "portable text decode error at byte {}: {}",
175                self.pos,
176                message.into()
177            ),
178        }
179    }
180
181    fn skip_ws(&mut self) {
182        while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_whitespace() {
183            self.pos += 1;
184        }
185    }
186
187    fn peek(&self) -> Option<u8> {
188        self.bytes.get(self.pos).copied()
189    }
190
191    fn bump(&mut self) -> Option<u8> {
192        let byte = self.peek()?;
193        self.pos += 1;
194        Some(byte)
195    }
196
197    fn expect(&mut self, byte: u8) -> Result<()> {
198        if self.bump() == Some(byte) {
199            Ok(())
200        } else {
201            Err(self.error(format!("expected '{}'", byte as char)))
202        }
203    }
204
205    fn parse_value(&mut self) -> Result<Expr> {
206        self.skip_ws();
207        match self.peek() {
208            Some(b'_') => {
209                self.pos += 1;
210                Ok(Expr::Nil)
211            }
212            Some(b'T') => {
213                self.pos += 1;
214                Ok(Expr::Bool(true))
215            }
216            Some(b'F') => {
217                self.pos += 1;
218                Ok(Expr::Bool(false))
219            }
220            Some(b'N') => {
221                self.pos += 1;
222                let domain = self.parse_symbol_payload()?;
223                let canonical = self.parse_qstr()?;
224                Ok(Expr::Number(NumberLiteral { domain, canonical }))
225            }
226            Some(b'S') => {
227                self.pos += 1;
228                Ok(Expr::Symbol(self.parse_symbol_payload()?))
229            }
230            Some(b'R') => {
231                self.pos += 1;
232                Ok(Expr::String(self.parse_qstr()?))
233            }
234            Some(b'B') => {
235                self.pos += 1;
236                let hex = self.parse_qstr()?;
237                Ok(Expr::Bytes(self.parse_hex(&hex)?))
238            }
239            Some(b'(') => Ok(Expr::List(self.parse_seq(b'(', b')')?)),
240            Some(b'[') => Ok(Expr::Vector(self.parse_seq(b'[', b']')?)),
241            Some(b'%') => {
242                self.pos += 1;
243                Ok(Expr::Set(self.parse_seq(b'(', b')')?))
244            }
245            Some(b'{') => self.parse_map(),
246            Some(other) => Err(self.error(format!("unexpected tag byte '{}'", other as char))),
247            None => Err(self.error("unexpected end of input")),
248        }
249    }
250
251    fn parse_seq(&mut self, open: u8, close: u8) -> Result<Vec<Expr>> {
252        self.expect(open)?;
253        let mut items = Vec::new();
254        loop {
255            self.skip_ws();
256            match self.peek() {
257                Some(byte) if byte == close => {
258                    self.pos += 1;
259                    return Ok(items);
260                }
261                None => return Err(self.error("unterminated sequence")),
262                _ => items.push(self.parse_value()?),
263            }
264        }
265    }
266
267    fn parse_map(&mut self) -> Result<Expr> {
268        self.expect(b'{')?;
269        let mut entries = Vec::new();
270        loop {
271            self.skip_ws();
272            match self.peek() {
273                Some(b'}') => {
274                    self.pos += 1;
275                    return Ok(Expr::Map(entries));
276                }
277                None => return Err(self.error("unterminated map")),
278                _ => {
279                    let key = self.parse_value()?;
280                    let value = self.parse_value()?;
281                    entries.push((key, value));
282                }
283            }
284        }
285    }
286
287    fn parse_symbol_payload(&mut self) -> Result<Symbol> {
288        match self.bump() {
289            Some(b'Q') => {
290                let namespace = self.parse_qstr()?;
291                let name = self.parse_qstr()?;
292                Ok(Symbol::qualified(namespace, name))
293            }
294            Some(b'U') => Ok(Symbol::new(self.parse_qstr()?)),
295            _ => Err(self.error("expected symbol payload tag 'Q' or 'U'")),
296        }
297    }
298
299    fn parse_qstr(&mut self) -> Result<String> {
300        self.expect(b'"')?;
301        let mut bytes = Vec::new();
302        loop {
303            match self.bump() {
304                Some(b'"') => {
305                    return String::from_utf8(bytes)
306                        .map_err(|err| self.error(format!("invalid utf-8 in string: {err}")));
307                }
308                Some(b'\\') => match self.bump() {
309                    Some(b'\\') => bytes.push(b'\\'),
310                    Some(b'"') => bytes.push(b'"'),
311                    Some(b'n') => bytes.push(b'\n'),
312                    Some(b'r') => bytes.push(b'\r'),
313                    Some(b't') => bytes.push(b'\t'),
314                    _ => return Err(self.error("invalid escape sequence")),
315                },
316                Some(byte) => bytes.push(byte),
317                None => return Err(self.error("unterminated string")),
318            }
319        }
320    }
321
322    fn parse_hex(&self, hex: &str) -> Result<Vec<u8>> {
323        if !hex.len().is_multiple_of(2) {
324            return Err(self.error("byte literal has odd hex length"));
325        }
326        let bytes = hex.as_bytes();
327        let mut out = Vec::with_capacity(hex.len() / 2);
328        let mut index = 0;
329        while index < bytes.len() {
330            let hi = hex_digit(bytes[index]).ok_or_else(|| self.error("invalid hex digit"))?;
331            let lo = hex_digit(bytes[index + 1]).ok_or_else(|| self.error("invalid hex digit"))?;
332            out.push((hi << 4) | lo);
333            index += 2;
334        }
335        Ok(out)
336    }
337}
338
339fn hex_digit(byte: u8) -> Option<u8> {
340    match byte {
341        b'0'..=b'9' => Some(byte - b'0'),
342        b'a'..=b'f' => Some(byte - b'a' + 10),
343        b'A'..=b'F' => Some(byte - b'A' + 10),
344        _ => None,
345    }
346}