Skip to main content

tidepool_runtime/
render.rs

1use serde_json::json;
2use tidepool_eval::value::Value;
3use tidepool_repr::datacon_table::DataConTable;
4use tidepool_repr::types::{DataConId, Literal};
5
6const MAX_DEPTH: usize = 1000;
7const MAX_LIST_LEN: usize = 10000;
8
9/// Opaque result of evaluating a Haskell expression.
10/// Bundles the computed `Value` with the `DataConTable` needed to render constructor names.
11#[derive(Debug)]
12pub struct EvalResult {
13    value: Value,
14    table: DataConTable,
15}
16
17impl EvalResult {
18    pub(crate) fn new(value: Value, table: DataConTable) -> Self {
19        Self { value, table }
20    }
21
22    /// Render the result as structured JSON.
23    pub fn to_json(&self) -> serde_json::Value {
24        value_to_json(&self.value, &self.table, 0)
25    }
26
27    /// Pretty-print the JSON representation.
28    pub fn to_string_pretty(&self) -> String {
29        let j = self.to_json();
30        // For simple scalars, use compact form
31        match &j {
32            serde_json::Value::Number(_) | serde_json::Value::Bool(_) | serde_json::Value::Null => {
33                j.to_string()
34            }
35            serde_json::Value::String(s) => {
36                // Check if it's a single char or short string — use compact
37                if s.len() <= 80 {
38                    j.to_string()
39                } else {
40                    serde_json::to_string_pretty(&j).unwrap_or_else(|_| j.to_string())
41                }
42            }
43            _ => serde_json::to_string_pretty(&j).unwrap_or_else(|_| j.to_string()),
44        }
45    }
46
47    /// Consume and return the inner Value (escape hatch for callers that need raw access).
48    pub fn into_value(self) -> Value {
49        self.value
50    }
51
52    /// Borrow the inner Value.
53    pub fn value(&self) -> &Value {
54        &self.value
55    }
56
57    /// Borrow the DataConTable.
58    pub fn table(&self) -> &DataConTable {
59        &self.table
60    }
61}
62
63fn con_name(id: DataConId, table: &DataConTable) -> &str {
64    table.name_of(id).unwrap_or("<unknown>")
65}
66
67/// Convert a tidepool Value to serde_json::Value using the DataConTable for constructor names.
68pub fn value_to_json(val: &Value, table: &DataConTable, depth: usize) -> serde_json::Value {
69    if depth > MAX_DEPTH {
70        return json!("<depth limit>");
71    }
72    let d = depth + 1;
73
74    match val {
75        // Literals
76        Value::Lit(lit) => literal_to_json(lit),
77
78        // Constructors — pattern match on known names
79        Value::Con(id, fields) => {
80            let name = con_name(*id, table);
81            match (name, fields.as_slice()) {
82                // Booleans
83                ("True", []) => json!(true),
84                ("False", []) => json!(false),
85
86                // Unit
87                ("()", []) => json!(null),
88
89                // Maybe
90                ("Nothing", []) => json!(null),
91                ("Just", [x]) => value_to_json(x, table, d),
92
93                // freer-simple Pure
94                ("Pure", [x]) => value_to_json(x, table, d),
95
96                // Boxing constructors: I#, W#, C#, D#, F#
97                ("I#", [x]) | ("W#", [x]) | ("D#", [x]) | ("F#", [x]) => value_to_json(x, table, d),
98                ("C#", [x]) => value_to_json(x, table, d),
99
100                // Text constructor: Text ByteArray off len → JSON string
101                // ByteArray# may be raw Value::ByteArray or lifted Con("ByteArray", [Value::ByteArray(..)])
102                ("Text", [ba_val, off_val, len_val]) => {
103                    // Recursively unwrap Con("ByteArray", [x]) layers to find
104                    // the raw ByteArray#. Sliced Text values (from splitOn etc.)
105                    // can produce multiple wrapping layers.
106                    let raw_ba = {
107                        let mut cur = ba_val;
108                        loop {
109                            match cur {
110                                Value::ByteArray(bs) => break Some(bs.clone()),
111                                Value::Con(id, fields)
112                                    if con_name(*id, table) == "ByteArray" && fields.len() == 1 =>
113                                {
114                                    cur = &fields[0];
115                                }
116                                _ => break None,
117                            }
118                        }
119                    };
120                    if let Some(bs) = raw_ba {
121                        let borrowed = bs.lock().unwrap_or_else(|e| e.into_inner());
122                        let off = extract_boxed_int(off_val, table).unwrap_or(0) as usize;
123                        let len = extract_boxed_int(len_val, table).unwrap_or(borrowed.len() as i64)
124                            as usize;
125                        let end = (off + len).min(borrowed.len());
126                        match std::str::from_utf8(&borrowed[off..end]) {
127                            Ok(s) => json!(s),
128                            Err(_) => json!(format!("<Text invalid UTF-8 len={}>", len)),
129                        }
130                    } else {
131                        let field_jsons: Vec<serde_json::Value> =
132                            fields.iter().map(|f| value_to_json(f, table, d)).collect();
133                        json!({"constructor": "Text", "fields": field_jsons})
134                    }
135                }
136
137                // List: try to collect as array or string
138                ("[]", []) => {
139                    // Empty list
140                    json!([])
141                }
142                (":", [head, tail]) => collect_list(head, tail, table, d),
143
144                // Tuples: (,), (,,), (,,,), etc.
145                (n, fields)
146                    if n.starts_with('(')
147                        && n.ends_with(')')
148                        && n.chars().all(|c| c == '(' || c == ')' || c == ',')
149                        && fields.len() >= 2 =>
150                {
151                    let elems: Vec<serde_json::Value> =
152                        fields.iter().map(|f| value_to_json(f, table, d)).collect();
153                    json!(elems)
154                }
155
156                // Integer constructors (GHC.Num.Integer)
157                // IS Int# — small integer (fits in machine word)
158                ("IS", [x]) => value_to_json(x, table, d),
159                // IP ByteArray# — positive big integer (not yet supported, show as string)
160                ("IP", _) => json!("<big-integer>"),
161                // IN ByteArray# — negative big integer (not yet supported, show as string)
162                ("IN", _) => json!("<big-integer>"),
163
164                // Scientific (Data.Scientific) — coefficient × 10^exponent
165                ("Scientific", [coeff, exp_val]) => {
166                    let c = match value_to_json(coeff, table, d) {
167                        serde_json::Value::Number(n) => n.as_i64().unwrap_or(0),
168                        _ => 0,
169                    };
170                    let e = match value_to_json(exp_val, table, d) {
171                        serde_json::Value::Number(n) => n.as_i64().unwrap_or(0),
172                        _ => 0,
173                    };
174                    // When exponent >= 0, produce an integer JSON number
175                    if e >= 0 {
176                        let val = c * 10i64.pow(e as u32);
177                        json!(val)
178                    } else {
179                        let val = c as f64 * 10f64.powi(e as i32);
180                        json!(val)
181                    }
182                }
183
184                // Aeson Value constructors
185                ("Null", []) => json!(null),
186                ("Bool", [x]) => value_to_json(x, table, d),
187                ("Number", [x]) => value_to_json(x, table, d),
188                ("String", [x]) => value_to_json(x, table, d),
189                ("Array", [vec_val]) => value_to_json(vec_val, table, d),
190                ("Object", [map_val]) => map_to_json_object(map_val, table, d),
191
192                // Data.Vector.Vector: worker-wrapper inlines fields as
193                // Vector Int# Int# (Array# a). The Array# contents come from
194                // heap_bridge as Con(DataConId(0), elems). Extract and render
195                // the elements directly rather than delegating to value_to_json
196                // (which would hit the generic constructor case for the nameless Con).
197                ("Vector", fields) => {
198                    // Find the Array# field: it's the Con(_, elems) with elements,
199                    // typically the last field (after Int# offset and length).
200                    let array_elems = fields.iter().rev().find_map(|f| match f {
201                        Value::Con(_, elems) if !elems.is_empty() => Some(elems),
202                        _ => None,
203                    });
204                    if let Some(elems) = array_elems {
205                        let arr: Vec<serde_json::Value> =
206                            elems.iter().map(|e| value_to_json(e, table, d)).collect();
207                        json!(arr)
208                    } else {
209                        json!([])
210                    }
211                }
212
213                // Generic constructor
214                (_name, fields) => {
215                    if fields.is_empty() {
216                        json!(name)
217                    } else {
218                        let field_jsons: Vec<serde_json::Value> =
219                            fields.iter().map(|f| value_to_json(f, table, d)).collect();
220                        json!({
221                            "constructor": name,
222                            "fields": field_jsons
223                        })
224                    }
225                }
226            }
227        }
228
229        // Closures / thunks — opaque
230        Value::Closure(_, _, _) => json!("<closure>"),
231        Value::ThunkRef(_) => json!("<thunk>"),
232        Value::JoinCont(_, _, _) => json!("<join>"),
233        Value::ConFun(id, _, _) => {
234            let name = con_name(*id, table);
235            json!(format!("<partially-applied {}>", name))
236        }
237        Value::ByteArray(bs) => {
238            let borrowed = bs.lock().unwrap_or_else(|e| e.into_inner());
239            match std::str::from_utf8(&borrowed) {
240                Ok(s) => json!(s),
241                Err(_) => json!(format!("<ByteArray# len={}>", borrowed.len())),
242            }
243        }
244    }
245}
246
247/// Walk a Data.Map.Strict Bin/Tip tree and collect key-value pairs into a JSON object.
248/// Keys are Text values (Key newtype is erased by GHC).
249fn map_to_json_object(val: &Value, table: &DataConTable, depth: usize) -> serde_json::Value {
250    let mut entries = serde_json::Map::new();
251    collect_map_entries(val, table, depth, &mut entries);
252    serde_json::Value::Object(entries)
253}
254
255fn collect_map_entries(
256    val: &Value,
257    table: &DataConTable,
258    depth: usize,
259    out: &mut serde_json::Map<String, serde_json::Value>,
260) {
261    if depth > MAX_DEPTH {
262        return;
263    }
264    if let Value::Con(id, fields) = val {
265        let name = con_name(*id, table);
266        match (name, fields.as_slice()) {
267            ("Tip", []) => {}
268            // Bin size key value left right
269            ("Bin", [_size, k, v, left, right]) => {
270                collect_map_entries(left, table, depth + 1, out);
271                let key_str = match value_to_json(k, table, depth + 1) {
272                    serde_json::Value::String(s) => s,
273                    other => other.to_string(),
274                };
275                out.insert(key_str, value_to_json(v, table, depth + 1));
276                collect_map_entries(right, table, depth + 1, out);
277            }
278            _ => {}
279        }
280    }
281}
282
283/// Extract an i64 from a potentially boxed Int value (LitInt or I#(I#(...(LitInt)))).
284/// Recursively unwraps nested Con("I#", [x]) layers.
285fn extract_boxed_int(val: &Value, table: &DataConTable) -> Option<i64> {
286    let mut cur = val;
287    loop {
288        match cur {
289            Value::Lit(Literal::LitInt(n)) => return Some(*n),
290            Value::Con(id, fields) if fields.len() == 1 && table.get_by_name("I#") == Some(*id) => {
291                cur = &fields[0];
292            }
293            _ => return None,
294        }
295    }
296}
297
298/// Try to extract a single char from a Value.
299/// Handles: LitChar, C#(LitChar), C#(Text(ByteArray(1 byte), 0, 1)).
300fn extract_char(val: &Value, table: &DataConTable) -> Option<char> {
301    match val {
302        Value::Lit(Literal::LitChar(c)) => Some(*c),
303        Value::Con(id, fields) if con_name(*id, table) == "C#" && fields.len() == 1 => {
304            extract_char_inner(&fields[0], table)
305        }
306        _ => None,
307    }
308}
309
310/// Extract a char from the inner value of a C# constructor (or bare value).
311fn extract_char_inner(val: &Value, table: &DataConTable) -> Option<char> {
312    match val {
313        Value::Lit(Literal::LitChar(c)) => Some(*c),
314        // Text(ByteArray#, off, len) where len == 1 — single-byte char
315        Value::Con(id, fields) if con_name(*id, table) == "Text" && fields.len() == 3 => {
316            let len = extract_boxed_int(&fields[2], table)?;
317            if len != 1 {
318                return None;
319            }
320            let off = extract_boxed_int(&fields[1], table).unwrap_or(0) as usize;
321            // Unwrap ByteArray layers to get the raw bytes
322            let raw_ba = {
323                let mut cur = &fields[0];
324                loop {
325                    match cur {
326                        Value::ByteArray(bs) => break Some(bs.clone()),
327                        Value::Con(cid, cfields)
328                            if con_name(*cid, table) == "ByteArray" && cfields.len() == 1 =>
329                        {
330                            cur = &cfields[0];
331                        }
332                        _ => break None,
333                    }
334                }
335            };
336            let bs = raw_ba?;
337            let borrowed = bs.lock().unwrap_or_else(|e| e.into_inner());
338            let byte = *borrowed.get(off)?;
339            Some(byte as char)
340        }
341        _ => None,
342    }
343}
344
345fn literal_to_json(lit: &Literal) -> serde_json::Value {
346    match lit {
347        Literal::LitInt(n) => json!(n),
348        Literal::LitWord(n) => json!(n),
349        Literal::LitChar(c) => json!(c.to_string()),
350        Literal::LitString(bytes) => match std::str::from_utf8(bytes) {
351            Ok(s) => json!(s),
352            Err(_) => json!(format!("<binary:{} bytes>", bytes.len())),
353        },
354        Literal::LitFloat(bits) => {
355            let f = f32::from_bits(*bits as u32) as f64;
356            if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
357                json!(f as i64)
358            } else {
359                serde_json::Number::from_f64(f)
360                    .map(serde_json::Value::Number)
361                    .unwrap_or(json!(null))
362            }
363        }
364        Literal::LitDouble(bits) => {
365            let f = f64::from_bits(*bits);
366            // If the double is integral and fits in i64, emit as integer
367            if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
368                json!(f as i64)
369            } else {
370                serde_json::Number::from_f64(f)
371                    .map(serde_json::Value::Number)
372                    .unwrap_or(json!(null))
373            }
374        }
375    }
376}
377
378/// Collect a cons-chain into a JSON array, or a JSON string if all elements are chars.
379fn collect_list(
380    head: &Value,
381    tail: &Value,
382    table: &DataConTable,
383    depth: usize,
384) -> serde_json::Value {
385    let mut elems = vec![head];
386    let mut current = tail;
387    let mut count = 1usize;
388
389    loop {
390        if count >= MAX_LIST_LEN {
391            // Truncate
392            let mut arr: Vec<serde_json::Value> = elems
393                .iter()
394                .map(|e| value_to_json(e, table, depth))
395                .collect();
396            arr.push(json!("..."));
397            return json!(arr);
398        }
399        match current {
400            Value::Con(id, fields) => {
401                let name = con_name(*id, table);
402                match (name, fields.as_slice()) {
403                    ("[]", []) => break,
404                    (":", [h, t]) => {
405                        elems.push(h);
406                        current = t;
407                        count += 1;
408                    }
409                    _ => {
410                        // Malformed list tail
411                        let mut arr: Vec<serde_json::Value> = elems
412                            .iter()
413                            .map(|e| value_to_json(e, table, depth))
414                            .collect();
415                        arr.push(value_to_json(current, table, depth));
416                        return json!(arr);
417                    }
418                }
419            }
420            _ => {
421                // Non-constructor tail (thunk, etc)
422                let mut arr: Vec<serde_json::Value> = elems
423                    .iter()
424                    .map(|e| value_to_json(e, table, depth))
425                    .collect();
426                arr.push(value_to_json(current, table, depth));
427                return json!(arr);
428            }
429        }
430    }
431
432    // Check if all elements are chars → render as string.
433    // Chars can appear as:
434    //   1. LitChar(c) — bare char literal
435    //   2. Con(C#, [LitChar(c)]) — boxed char
436    //   3. Con(C#, [Con(Text, [ByteArray(1 byte), 0, 1])]) — char as single-byte Text
437    let mut all_chars = true;
438    let mut char_buf = String::new();
439    for e in &elems {
440        if let Some(c) = extract_char(e, table) {
441            char_buf.push(c);
442        } else {
443            all_chars = false;
444            break;
445        }
446    }
447
448    if all_chars && !char_buf.is_empty() {
449        json!(char_buf)
450    } else {
451        let arr: Vec<serde_json::Value> = elems
452            .iter()
453            .map(|e| value_to_json(e, table, depth))
454            .collect();
455        json!(arr)
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use std::sync::{Arc, Mutex};
463    use tidepool_repr::datacon::DataCon;
464    use tidepool_repr::types::DataConId;
465
466    fn test_table() -> DataConTable {
467        let mut t = DataConTable::new();
468        let cons = [
469            (0, "Nothing", 0),
470            (1, "Just", 1),
471            (2, "True", 0),
472            (3, "False", 0),
473            (4, "()", 0),
474            (5, "I#", 1),
475            (6, "C#", 1),
476            (7, ":", 2),
477            (8, "[]", 0),
478            (9, "Text", 3),
479            (10, "(,)", 2),
480            (11, "(,,)", 3),
481            (12, "ByteArray", 1),
482        ];
483        for (id, name, arity) in cons {
484            t.insert(DataCon {
485                id: DataConId(id),
486                name: name.into(),
487                tag: id as u32,
488                rep_arity: arity,
489                field_bangs: vec![],
490                qualified_name: None,
491            });
492        }
493        t
494    }
495
496    #[test]
497    fn test_render_lit_int() {
498        let table = test_table();
499        let val = Value::Lit(Literal::LitInt(42));
500        assert_eq!(value_to_json(&val, &table, 0), json!(42));
501    }
502
503    #[test]
504    fn test_render_lit_string() {
505        let table = test_table();
506        let val = Value::Lit(Literal::LitString(b"hello".to_vec()));
507        assert_eq!(value_to_json(&val, &table, 0), json!("hello"));
508    }
509
510    #[test]
511    fn test_render_bool() {
512        let table = test_table();
513        let true_val = Value::Con(table.get_by_name("True").unwrap(), vec![]);
514        let false_val = Value::Con(table.get_by_name("False").unwrap(), vec![]);
515        assert_eq!(value_to_json(&true_val, &table, 0), json!(true));
516        assert_eq!(value_to_json(&false_val, &table, 0), json!(false));
517    }
518
519    #[test]
520    fn test_render_option() {
521        let table = test_table();
522        let nothing = Value::Con(table.get_by_name("Nothing").unwrap(), vec![]);
523        let just = Value::Con(
524            table.get_by_name("Just").unwrap(),
525            vec![Value::Lit(Literal::LitInt(42))],
526        );
527        assert_eq!(value_to_json(&nothing, &table, 0), json!(null));
528        assert_eq!(value_to_json(&just, &table, 0), json!(42));
529    }
530
531    #[test]
532    fn test_render_unit() {
533        let table = test_table();
534        let unit = Value::Con(table.get_by_name("()").unwrap(), vec![]);
535        assert_eq!(value_to_json(&unit, &table, 0), json!(null));
536    }
537
538    #[test]
539    fn test_render_list_int() {
540        let table = test_table();
541        let nil_id = table.get_by_name("[]").unwrap();
542        let cons_id = table.get_by_name(":").unwrap();
543
544        // [1, 2]
545        let list = Value::Con(
546            cons_id,
547            vec![
548                Value::Lit(Literal::LitInt(1)),
549                Value::Con(
550                    cons_id,
551                    vec![Value::Lit(Literal::LitInt(2)), Value::Con(nil_id, vec![])],
552                ),
553            ],
554        );
555        assert_eq!(value_to_json(&list, &table, 0), json!([1, 2]));
556    }
557
558    #[test]
559    fn test_render_text() {
560        let table = test_table();
561        let text_id = table.get_by_name("Text").unwrap();
562        let ba = Value::ByteArray(Arc::new(Mutex::new(b"hello".to_vec())));
563        let val = Value::Con(
564            text_id,
565            vec![
566                ba,
567                Value::Lit(Literal::LitInt(0)),
568                Value::Lit(Literal::LitInt(5)),
569            ],
570        );
571        assert_eq!(value_to_json(&val, &table, 0), json!("hello"));
572    }
573
574    #[test]
575    fn test_render_list_string() {
576        let table = test_table();
577        let nil_id = table.get_by_name("[]").unwrap();
578        let cons_id = table.get_by_name(":").unwrap();
579
580        // ["a", "b"]
581        let list = Value::Con(
582            cons_id,
583            vec![
584                Value::Lit(Literal::LitString(b"a".to_vec())),
585                Value::Con(
586                    cons_id,
587                    vec![
588                        Value::Lit(Literal::LitString(b"b".to_vec())),
589                        Value::Con(nil_id, vec![]),
590                    ],
591                ),
592            ],
593        );
594        assert_eq!(value_to_json(&list, &table, 0), json!(["a", "b"]));
595    }
596
597    #[test]
598    fn test_render_tuple() {
599        let table = test_table();
600        let pair_id = table.get_by_name("(,)").unwrap();
601        let triple_id = table.get_by_name("(,,)").unwrap();
602
603        let pair = Value::Con(
604            pair_id,
605            vec![
606                Value::Lit(Literal::LitInt(1)),
607                Value::Lit(Literal::LitInt(2)),
608            ],
609        );
610        let triple = Value::Con(
611            triple_id,
612            vec![
613                Value::Lit(Literal::LitInt(1)),
614                Value::Lit(Literal::LitInt(2)),
615                Value::Lit(Literal::LitInt(3)),
616            ],
617        );
618
619        assert_eq!(value_to_json(&pair, &table, 0), json!([1, 2]));
620        assert_eq!(value_to_json(&triple, &table, 0), json!([1, 2, 3]));
621    }
622
623    #[test]
624    fn test_render_char_list_as_string() {
625        let table = test_table();
626        let nil_id = table.get_by_name("[]").unwrap();
627        let cons_id = table.get_by_name(":").unwrap();
628        let c_hash_id = table.get_by_name("C#").unwrap();
629
630        // ['h', 'i']
631        let list = Value::Con(
632            cons_id,
633            vec![
634                Value::Con(c_hash_id, vec![Value::Lit(Literal::LitChar('h'))]),
635                Value::Con(
636                    cons_id,
637                    vec![
638                        Value::Con(c_hash_id, vec![Value::Lit(Literal::LitChar('i'))]),
639                        Value::Con(nil_id, vec![]),
640                    ],
641                ),
642            ],
643        );
644        assert_eq!(value_to_json(&list, &table, 0), json!("hi"));
645    }
646}