Skip to main content

sim_codec_json/
expr_json.rs

1//! The canonical, lossless `Expr <-> JSON` projection.
2//!
3//! Maps each kernel `Expr` variant to and from a `$expr`-tagged
4//! `serde_json::Value` so that every expression round-trips exactly.
5
6use serde_json::{Value as JsonValue, json};
7use sim_codec::DecodeBudget;
8use sim_kernel::{Expr, NumberLiteral, Result};
9
10use crate::helpers::{
11    base64_decode, base64_encode, json_error, json_to_symbol, local_to_json, parse_quote_mode,
12    parse_symbol, quote_mode_name, required_array, required_bool, required_str, required_value,
13    symbol_from_json, symbol_to_json,
14};
15
16/// Projects an [`Expr`] onto a canonical `$expr`-tagged `serde_json::Value`.
17///
18/// This is the lossless encode half of the projection: every kernel `Expr`
19/// variant maps to a tagged JSON form that [`json_to_expr`] reads back exactly.
20pub fn expr_to_json(expr: &Expr) -> JsonValue {
21    match expr {
22        Expr::Nil => json!({ "$expr": "nil" }),
23        Expr::Bool(value) => json!({ "$expr": "bool", "value": value }),
24        Expr::Number(number) => json!({
25            "$expr": "number",
26            "domain": number.domain.to_string(),
27            "value": number.canonical,
28        }),
29        Expr::Symbol(symbol) => symbol_to_json(symbol),
30        Expr::Local(symbol) => local_to_json(symbol),
31        Expr::String(value) => json!({ "$expr": "string", "value": value }),
32        Expr::Bytes(value) => json!({
33            "$expr": "bytes",
34            "base64": base64_encode(value),
35        }),
36        Expr::List(items) => sequence_json("list", items),
37        Expr::Vector(items) => sequence_json("vector", items),
38        Expr::Map(entries) => {
39            let mut sorted = entries.clone();
40            sorted.sort_by_key(|(key, value)| (key.canonical_key(), value.canonical_key()));
41            json!({
42                "$expr": "map",
43                "entries": sorted
44                    .iter()
45                    .map(|(key, value)| json!({
46                        "key": expr_to_json(key),
47                        "value": expr_to_json(value),
48                    }))
49                    .collect::<Vec<_>>(),
50            })
51        }
52        Expr::Set(items) => {
53            let mut sorted = items.clone();
54            sorted.sort_by_key(Expr::canonical_key);
55            sequence_json("set", &sorted)
56        }
57        Expr::Call { operator, args } => json!({
58            "$expr": "call",
59            "operator": expr_to_json(operator),
60            "args": args.iter().map(expr_to_json).collect::<Vec<_>>(),
61        }),
62        Expr::Infix {
63            operator,
64            left,
65            right,
66        } => json!({
67            "$expr": "infix",
68            "operator": symbol_to_json(operator),
69            "left": expr_to_json(left),
70            "right": expr_to_json(right),
71        }),
72        Expr::Prefix { operator, arg } => json!({
73            "$expr": "prefix",
74            "operator": symbol_to_json(operator),
75            "arg": expr_to_json(arg),
76        }),
77        Expr::Postfix { operator, arg } => json!({
78            "$expr": "postfix",
79            "operator": symbol_to_json(operator),
80            "arg": expr_to_json(arg),
81        }),
82        Expr::Block(items) => sequence_json("block", items),
83        Expr::Quote { mode, expr } => json!({
84            "$expr": "quote",
85            "mode": quote_mode_name(*mode),
86            "expr": expr_to_json(expr),
87        }),
88        Expr::Annotated { expr, annotations } => json!({
89            "$expr": "annotated",
90            "expr": expr_to_json(expr),
91            "annotations": annotations
92                .iter()
93                .map(|(key, value)| json!({
94                    "key": symbol_to_json(key),
95                    "value": expr_to_json(value),
96                }))
97                .collect::<Vec<_>>(),
98        }),
99        Expr::Extension { tag, payload } => json!({
100            "$expr": "extension",
101            "tag": symbol_to_json(tag),
102            "payload": expr_to_json(payload),
103        }),
104    }
105}
106
107/// Reads a canonical `$expr`-tagged `serde_json::Value` back into an [`Expr`].
108///
109/// This is the lossless decode half of the projection, inverting
110/// [`expr_to_json`]; it enforces the decode `budget` and node `depth` so
111/// adversarial input cannot exhaust resources.
112pub fn json_to_expr(
113    codec: sim_kernel::CodecId,
114    value: &JsonValue,
115    budget: &mut DecodeBudget,
116    depth: usize,
117) -> Result<Expr> {
118    budget.enter_node(codec, depth)?;
119    let Some(tag) = value.get("$expr").and_then(JsonValue::as_str) else {
120        return Err(json_error(codec, "missing $expr"));
121    };
122
123    match tag {
124        "nil" => Ok(Expr::Nil),
125        "bool" => Ok(Expr::Bool(required_bool(codec, value, "value")?)),
126        "number" => Ok(Expr::Number(NumberLiteral {
127            domain: parse_symbol(required_str(codec, value, "domain")?),
128            canonical: required_str(codec, value, "value")?.to_owned(),
129        })),
130        "symbol" => Ok(json_to_symbol(codec, value)?),
131        "local" => Ok(Expr::Local(symbol_from_json(codec, value)?)),
132        "string" => {
133            let text = required_str(codec, value, "value")?;
134            budget.check_string_bytes(codec, text.len())?;
135            Ok(Expr::String(text.to_owned()))
136        }
137        "bytes" => {
138            let encoded = required_str(codec, value, "base64")?;
139            budget.check_blob_bytes(codec, encoded.len())?;
140            let decoded = base64_decode(codec, encoded)?;
141            budget.check_blob_bytes(codec, decoded.len())?;
142            Ok(Expr::Bytes(decoded))
143        }
144        "list" => Ok(Expr::List(read_items(
145            codec,
146            value,
147            "items",
148            budget,
149            depth + 1,
150        )?)),
151        "vector" => Ok(Expr::Vector(read_items(
152            codec,
153            value,
154            "items",
155            budget,
156            depth + 1,
157        )?)),
158        "map" => {
159            let entries = required_array(codec, value, "entries")?;
160            budget.check_collection_len(codec, entries.len())?;
161            Ok(Expr::Map(
162                entries
163                    .iter()
164                    .map(|entry| {
165                        Ok((
166                            json_to_expr(
167                                codec,
168                                required_value(codec, entry, "key")?,
169                                budget,
170                                depth + 1,
171                            )?,
172                            json_to_expr(
173                                codec,
174                                required_value(codec, entry, "value")?,
175                                budget,
176                                depth + 1,
177                            )?,
178                        ))
179                    })
180                    .collect::<Result<Vec<_>>>()?,
181            ))
182        }
183        "set" => Ok(Expr::Set(read_items(
184            codec,
185            value,
186            "items",
187            budget,
188            depth + 1,
189        )?)),
190        "call" => Ok(Expr::Call {
191            operator: Box::new(json_to_expr(
192                codec,
193                required_value(codec, value, "operator")?,
194                budget,
195                depth + 1,
196            )?),
197            args: read_items(codec, value, "args", budget, depth + 1)?,
198        }),
199        "infix" => Ok(Expr::Infix {
200            operator: symbol_from_json(codec, required_value(codec, value, "operator")?)?,
201            left: Box::new(json_to_expr(
202                codec,
203                required_value(codec, value, "left")?,
204                budget,
205                depth + 1,
206            )?),
207            right: Box::new(json_to_expr(
208                codec,
209                required_value(codec, value, "right")?,
210                budget,
211                depth + 1,
212            )?),
213        }),
214        "prefix" => Ok(Expr::Prefix {
215            operator: symbol_from_json(codec, required_value(codec, value, "operator")?)?,
216            arg: Box::new(json_to_expr(
217                codec,
218                required_value(codec, value, "arg")?,
219                budget,
220                depth + 1,
221            )?),
222        }),
223        "postfix" => Ok(Expr::Postfix {
224            operator: symbol_from_json(codec, required_value(codec, value, "operator")?)?,
225            arg: Box::new(json_to_expr(
226                codec,
227                required_value(codec, value, "arg")?,
228                budget,
229                depth + 1,
230            )?),
231        }),
232        "block" => Ok(Expr::Block(read_items(
233            codec,
234            value,
235            "items",
236            budget,
237            depth + 1,
238        )?)),
239        "quote" => Ok(Expr::Quote {
240            mode: parse_quote_mode(codec, required_str(codec, value, "mode")?)?,
241            expr: Box::new(json_to_expr(
242                codec,
243                required_value(codec, value, "expr")?,
244                budget,
245                depth + 1,
246            )?),
247        }),
248        "annotated" => {
249            let annotations = required_array(codec, value, "annotations")?;
250            budget.check_collection_len(codec, annotations.len())?;
251            Ok(Expr::Annotated {
252                expr: Box::new(json_to_expr(
253                    codec,
254                    required_value(codec, value, "expr")?,
255                    budget,
256                    depth + 1,
257                )?),
258                annotations: annotations
259                    .iter()
260                    .map(|entry| {
261                        Ok((
262                            symbol_from_json(codec, required_value(codec, entry, "key")?)?,
263                            json_to_expr(
264                                codec,
265                                required_value(codec, entry, "value")?,
266                                budget,
267                                depth + 1,
268                            )?,
269                        ))
270                    })
271                    .collect::<Result<Vec<_>>>()?,
272            })
273        }
274        "extension" => Ok(Expr::Extension {
275            tag: symbol_from_json(codec, required_value(codec, value, "tag")?)?,
276            payload: Box::new(json_to_expr(
277                codec,
278                required_value(codec, value, "payload")?,
279                budget,
280                depth + 1,
281            )?),
282        }),
283        other => Err(json_error(codec, format!("unknown expr tag {other}"))),
284    }
285}
286
287fn sequence_json(tag: &'static str, items: &[Expr]) -> JsonValue {
288    json!({
289        "$expr": tag,
290        "items": items.iter().map(expr_to_json).collect::<Vec<_>>(),
291    })
292}
293
294fn read_items(
295    codec: sim_kernel::CodecId,
296    value: &JsonValue,
297    field: &str,
298    budget: &mut DecodeBudget,
299    depth: usize,
300) -> Result<Vec<Expr>> {
301    let items = required_array(codec, value, field)?;
302    budget.check_collection_len(codec, items.len())?;
303    items
304        .iter()
305        .map(|item| json_to_expr(codec, item, budget, depth))
306        .collect()
307}