Skip to main content

tidepool_bridge/
json.rs

1//! Bridge between `serde_json::Value` and Tidepool Core values.
2//!
3//! Converts serde_json JSON values to the vendored Tidepool.Aeson.Value ADT
4//! representation in Core. Constructor names match exactly:
5//!   Value = Object | Array | String | Number | Bool | Null
6//!
7//! KeyMap is backed by Data.Map.Strict (Map Key Value), so objects are
8//! represented as balanced binary trees of (Key, Value) pairs.
9
10use crate::error::BridgeError;
11use crate::traits::ToCore;
12use tidepool_eval::Value;
13use tidepool_repr::{DataConTable, Literal};
14
15/// Convert a `serde_json::Value` to a Tidepool Core `Value` matching the
16/// vendored `Tidepool.Aeson.Value` Haskell type.
17///
18/// The resulting Core value can be passed to Haskell code that expects
19/// `Value` (the aeson-compatible type) and accessed via lens combinators.
20impl ToCore for serde_json::Value {
21    fn to_value(&self, table: &DataConTable) -> Result<Value, BridgeError> {
22        // Use get_by_name_arity to disambiguate aeson Value constructors from
23        // GHC-internal types that share the same unqualified name (e.g. "Array").
24        match self {
25            serde_json::Value::Null => {
26                let id = table
27                    .get_by_name_arity("Null", 0)
28                    .ok_or_else(|| BridgeError::UnknownDataConName("Null".into()))?;
29                Ok(Value::Con(id, vec![]))
30            }
31
32            serde_json::Value::Bool(b) => {
33                let id = table
34                    .get_by_name_arity("Bool", 1)
35                    .ok_or_else(|| BridgeError::UnknownDataConName("Bool".into()))?;
36                let inner = (*b).to_value(table)?;
37                Ok(Value::Con(id, vec![inner]))
38            }
39
40            serde_json::Value::Number(n) => {
41                let id = table
42                    .get_by_name_arity("Number", 1)
43                    .ok_or_else(|| BridgeError::UnknownDataConName("Number".into()))?;
44                let f = n.as_f64().unwrap_or(0.0);
45                Ok(Value::Con(
46                    id,
47                    vec![Value::Lit(Literal::LitDouble(f.to_bits()))],
48                ))
49            }
50
51            serde_json::Value::String(s) => {
52                let id = table
53                    .get_by_name_arity("String", 1)
54                    .ok_or_else(|| BridgeError::UnknownDataConName("String".into()))?;
55                let inner = s.clone().to_value(table)?;
56                Ok(Value::Con(id, vec![inner]))
57            }
58
59            serde_json::Value::Array(arr) => {
60                let id = table
61                    .get_by_name_arity("Array", 1)
62                    .ok_or_else(|| BridgeError::UnknownDataConName("Array".into()))?;
63                // Vendored Value uses [Value] for Array (cons-list, not Vector).
64                let elements: Result<Vec<Value>, BridgeError> =
65                    arr.iter().map(|v| v.to_value(table)).collect();
66                let list = elements?.to_value(table)?;
67                Ok(Value::Con(id, vec![list]))
68            }
69
70            serde_json::Value::Object(map) => {
71                let id = table
72                    .get_by_name_arity("Object", 1)
73                    .ok_or_else(|| BridgeError::UnknownDataConName("Object".into()))?;
74                // KeyMap = Map Key Value (backed by Data.Map.Strict)
75                // Map is a balanced binary tree:
76                //   data Map k v = Bin !Int !k !v !(Map k v) !(Map k v) | Tip
77                let map_val = keymap_to_value(map, table)?;
78                Ok(Value::Con(id, vec![map_val]))
79            }
80        }
81    }
82}
83
84/// Build a Data.Map.Strict.Map Key Value from a serde_json Map.
85///
86/// Map is:
87///   Bin :: Int -> k -> v -> Map k v -> Map k v -> Map k v
88///   Tip :: Map k v
89///
90/// We build a balanced tree by sorting keys and using divide-and-conquer.
91fn keymap_to_value(
92    map: &serde_json::Map<std::string::String, serde_json::Value>,
93    table: &DataConTable,
94) -> Result<Value, BridgeError> {
95    // Prefer qualified name lookup; fall back to arity-based for legacy CBOR.
96    let bin_id = table
97        .get_by_qualified_name("Data.Map.Bin")
98        .or_else(|| table.get_by_name_arity("Bin", 5))
99        .ok_or_else(|| BridgeError::UnknownDataConName("Bin".into()))?;
100    let tip_id = table
101        .get_by_qualified_name("Data.Map.Tip")
102        .or_else(|| table.get_companion(bin_id, "Tip", 0))
103        .or_else(|| table.get_by_name_arity("Tip", 0))
104        .ok_or_else(|| BridgeError::UnknownDataConName("Tip".into()))?;
105    // Bin's first field is !Int — boxed on the heap as I#(Int#)
106    let i_hash_id = table
107        .get_by_name_arity("I#", 1)
108        .ok_or_else(|| BridgeError::UnknownDataConName("I#".into()))?;
109
110    // Collect and sort entries by key for balanced tree construction
111    let mut entries: Vec<(&std::string::String, &serde_json::Value)> = map.iter().collect();
112    entries.sort_by(|a, b| a.0.cmp(b.0));
113
114    fn build_tree(
115        entries: &[(&std::string::String, &serde_json::Value)],
116        bin_id: tidepool_repr::DataConId,
117        tip_id: tidepool_repr::DataConId,
118        i_hash_id: tidepool_repr::DataConId,
119        table: &DataConTable,
120    ) -> Result<Value, BridgeError> {
121        if entries.is_empty() {
122            return Ok(Value::Con(tip_id, vec![]));
123        }
124        let mid = entries.len() / 2;
125        let (k, v) = entries[mid];
126        let left = build_tree(&entries[..mid], bin_id, tip_id, i_hash_id, table)?;
127        let right = build_tree(&entries[mid + 1..], bin_id, tip_id, i_hash_id, table)?;
128
129        // Key is a newtype for Text — GHC erases it, so store plain Text
130        let key_val = k.clone().to_value(table)?;
131
132        let json_val = v.to_value(table)?;
133        // Bin's !Int field must be boxed as I#(n) to match GHC's heap representation
134        let size = Value::Con(
135            i_hash_id,
136            vec![Value::Lit(Literal::LitInt(entries.len() as i64))],
137        );
138
139        // Bin size key value left right
140        Ok(Value::Con(
141            bin_id,
142            vec![size, key_val, json_val, left, right],
143        ))
144    }
145
146    build_tree(&entries, bin_id, tip_id, i_hash_id, table)
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use tidepool_repr::{DataCon, DataConId};
153
154    /// Build a DataConTable with all constructors needed for JSON values.
155    fn json_test_table() -> DataConTable {
156        let mut t = DataConTable::new();
157        let cons = [
158            // Value constructors
159            ("Object", 0, 1),
160            ("Array", 1, 1),
161            ("String", 2, 1),
162            ("Number", 3, 1),
163            ("Bool", 4, 1),
164            ("Null", 5, 0),
165            // Map constructors
166            ("Bin", 6, 5),
167            ("Tip", 7, 0),
168            // Bool values
169            ("True", 8, 0),
170            ("False", 9, 0),
171            // List
172            ("[]", 10, 0),
173            (":", 11, 2),
174            // Text
175            ("Text", 12, 3),
176            // Int boxing
177            ("I#", 13, 1),
178        ];
179
180        for (i, (name, tag, arity)) in cons.iter().enumerate() {
181            t.insert(DataCon {
182                id: DataConId(i as u64),
183                name: (*name).into(),
184                tag: *tag,
185                rep_arity: *arity,
186                field_bangs: vec![],
187                qualified_name: None,
188            });
189        }
190        t
191    }
192
193    #[test]
194    fn test_null_to_value() {
195        let table = json_test_table();
196        let json = serde_json::Value::Null;
197        let val = json.to_value(&table).unwrap();
198        match val {
199            Value::Con(id, fields) => {
200                assert_eq!(table.name_of(id), Some("Null"));
201                assert!(fields.is_empty());
202            }
203            _ => panic!("Expected Con(Null)"),
204        }
205    }
206
207    #[test]
208    fn test_bool_to_value() {
209        let table = json_test_table();
210        let json = serde_json::json!(true);
211        let val = json.to_value(&table).unwrap();
212        match &val {
213            Value::Con(id, fields) => {
214                assert_eq!(table.name_of(*id), Some("Bool"));
215                assert_eq!(fields.len(), 1);
216                // Inner should be True constructor
217                match &fields[0] {
218                    Value::Con(inner_id, _) => {
219                        assert_eq!(table.name_of(*inner_id), Some("True"));
220                    }
221                    _ => panic!("Expected Con(True)"),
222                }
223            }
224            _ => panic!("Expected Con(Bool)"),
225        }
226    }
227
228    #[test]
229    fn test_string_to_value() {
230        let table = json_test_table();
231        let json = serde_json::json!("hello");
232        let val = json.to_value(&table).unwrap();
233        match &val {
234            Value::Con(id, fields) => {
235                assert_eq!(table.name_of(*id), Some("String"));
236                assert_eq!(fields.len(), 1);
237                // Inner should be Text constructor
238                match &fields[0] {
239                    Value::Con(inner_id, _) => {
240                        assert_eq!(table.name_of(*inner_id), Some("Text"));
241                    }
242                    _ => panic!("Expected Con(Text), got {:?}", fields[0]),
243                }
244            }
245            _ => panic!("Expected Con(String)"),
246        }
247    }
248
249    #[test]
250    fn test_number_to_value() {
251        let table = json_test_table();
252        let json = serde_json::json!(42);
253        let val = json.to_value(&table).unwrap();
254        match &val {
255            Value::Con(id, fields) => {
256                assert_eq!(table.name_of(*id), Some("Number"));
257                assert_eq!(fields.len(), 1);
258                // Inner should be LitDouble
259                match &fields[0] {
260                    Value::Lit(Literal::LitDouble(bits)) => {
261                        assert_eq!(f64::from_bits(*bits), 42.0);
262                    }
263                    _ => panic!("Expected Lit(LitDouble), got {:?}", fields[0]),
264                }
265            }
266            _ => panic!("Expected Con(Number)"),
267        }
268    }
269
270    #[test]
271    fn test_number_double_to_value() {
272        let table = json_test_table();
273        let json = serde_json::json!(3.14);
274        let val = json.to_value(&table).unwrap();
275        match &val {
276            Value::Con(id, fields) => {
277                assert_eq!(table.name_of(*id), Some("Number"));
278                assert_eq!(fields.len(), 1);
279                match &fields[0] {
280                    Value::Lit(Literal::LitDouble(bits)) => {
281                        let f = f64::from_bits(*bits);
282                        assert!((f - 3.14).abs() < 1e-10);
283                    }
284                    _ => panic!("Expected Lit(LitDouble), got {:?}", fields[0]),
285                }
286            }
287            _ => panic!("Expected Con(Number)"),
288        }
289    }
290
291    #[test]
292    fn test_array_to_value() {
293        let table = json_test_table();
294        let json = serde_json::json!([1, 2, 3]);
295        let val = json.to_value(&table).unwrap();
296        match &val {
297            Value::Con(id, fields) => {
298                assert_eq!(table.name_of(*id), Some("Array"));
299                assert_eq!(fields.len(), 1);
300                // Inner should be a cons list
301            }
302            _ => panic!("Expected Con(Array)"),
303        }
304    }
305
306    #[test]
307    fn test_object_to_value() {
308        let table = json_test_table();
309        let json = serde_json::json!({"name": "Alice", "age": 30});
310        let val = json.to_value(&table).unwrap();
311        match &val {
312            Value::Con(id, fields) => {
313                assert_eq!(table.name_of(*id), Some("Object"));
314                assert_eq!(fields.len(), 1);
315                // Inner should be a Map (Bin or Tip)
316            }
317            _ => panic!("Expected Con(Object)"),
318        }
319    }
320
321    #[test]
322    fn test_nested_json() {
323        let table = json_test_table();
324        let json = serde_json::json!({
325            "users": [
326                {"name": "Alice", "active": true},
327                {"name": "Bob", "active": false}
328            ],
329            "count": 2
330        });
331        let val = json.to_value(&table).unwrap();
332        match &val {
333            Value::Con(id, _) => {
334                assert_eq!(table.name_of(*id), Some("Object"));
335            }
336            _ => panic!("Expected Con(Object)"),
337        }
338    }
339
340    // --- Exhaustive round-trip tests ---
341
342    #[test]
343    fn test_bool_false_to_value() {
344        let table = json_test_table();
345        let val = serde_json::json!(false).to_value(&table).unwrap();
346        match &val {
347            Value::Con(id, fields) => {
348                assert_eq!(table.name_of(*id), Some("Bool"));
349                match &fields[0] {
350                    Value::Con(inner_id, _) => assert_eq!(table.name_of(*inner_id), Some("False")),
351                    _ => panic!("Expected Con(False)"),
352                }
353            }
354            _ => panic!("Expected Con(Bool)"),
355        }
356    }
357
358    #[test]
359    fn test_number_negative() {
360        let table = json_test_table();
361        let val = serde_json::json!(-1).to_value(&table).unwrap();
362        match &val {
363            Value::Con(id, fields) => {
364                assert_eq!(table.name_of(*id), Some("Number"));
365                match &fields[0] {
366                    Value::Lit(Literal::LitDouble(bits)) => {
367                        assert_eq!(f64::from_bits(*bits), -1.0);
368                    }
369                    _ => panic!("Expected LitDouble"),
370                }
371            }
372            _ => panic!("Expected Con(Number)"),
373        }
374    }
375
376    #[test]
377    fn test_number_zero() {
378        let table = json_test_table();
379        let val = serde_json::json!(0).to_value(&table).unwrap();
380        match &val {
381            Value::Con(id, fields) => {
382                assert_eq!(table.name_of(*id), Some("Number"));
383                match &fields[0] {
384                    Value::Lit(Literal::LitDouble(bits)) => {
385                        assert_eq!(f64::from_bits(*bits), 0.0);
386                    }
387                    _ => panic!("Expected LitDouble"),
388                }
389            }
390            _ => panic!("Expected Con(Number)"),
391        }
392    }
393
394    #[test]
395    fn test_string_empty() {
396        let table = json_test_table();
397        let val = serde_json::json!("").to_value(&table).unwrap();
398        match &val {
399            Value::Con(id, fields) => {
400                assert_eq!(table.name_of(*id), Some("String"));
401                assert_eq!(fields.len(), 1);
402                match &fields[0] {
403                    Value::Con(inner_id, _) => {
404                        assert_eq!(table.name_of(*inner_id), Some("Text"));
405                    }
406                    _ => panic!("Expected Con(Text)"),
407                }
408            }
409            _ => panic!("Expected Con(String)"),
410        }
411    }
412
413    #[test]
414    fn test_string_unicode() {
415        let table = json_test_table();
416        let val = serde_json::json!("héllo 🌊").to_value(&table).unwrap();
417        match &val {
418            Value::Con(id, fields) => {
419                assert_eq!(table.name_of(*id), Some("String"));
420                assert_eq!(fields.len(), 1);
421            }
422            _ => panic!("Expected Con(String)"),
423        }
424    }
425
426    #[test]
427    fn test_array_empty() {
428        let table = json_test_table();
429        let val = serde_json::json!([]).to_value(&table).unwrap();
430        match &val {
431            Value::Con(id, fields) => {
432                assert_eq!(table.name_of(*id), Some("Array"));
433                assert_eq!(fields.len(), 1);
434                // Inner should be empty list ([] constructor)
435                match &fields[0] {
436                    Value::Con(nil_id, nil_fields) => {
437                        assert_eq!(table.name_of(*nil_id), Some("[]"));
438                        assert!(nil_fields.is_empty());
439                    }
440                    _ => panic!("Expected Con([])"),
441                }
442            }
443            _ => panic!("Expected Con(Array)"),
444        }
445    }
446
447    #[test]
448    fn test_array_single() {
449        let table = json_test_table();
450        let val = serde_json::json!([1]).to_value(&table).unwrap();
451        match &val {
452            Value::Con(id, fields) => {
453                assert_eq!(table.name_of(*id), Some("Array"));
454                // Inner should be a cons cell (:)
455                match &fields[0] {
456                    Value::Con(cons_id, cons_fields) => {
457                        assert_eq!(table.name_of(*cons_id), Some(":"));
458                        assert_eq!(cons_fields.len(), 2);
459                        // Head should be Number(1)
460                        match &cons_fields[0] {
461                            Value::Con(num_id, _) => {
462                                assert_eq!(table.name_of(*num_id), Some("Number"));
463                            }
464                            _ => panic!("Expected Con(Number)"),
465                        }
466                        // Tail should be []
467                        match &cons_fields[1] {
468                            Value::Con(nil_id, _) => {
469                                assert_eq!(table.name_of(*nil_id), Some("[]"));
470                            }
471                            _ => panic!("Expected Con([])"),
472                        }
473                    }
474                    _ => panic!("Expected Con(:)"),
475                }
476            }
477            _ => panic!("Expected Con(Array)"),
478        }
479    }
480
481    #[test]
482    fn test_array_nested() {
483        let table = json_test_table();
484        let val = serde_json::json!([[1, 2], [3]]).to_value(&table).unwrap();
485        match &val {
486            Value::Con(id, _) => assert_eq!(table.name_of(*id), Some("Array")),
487            _ => panic!("Expected Con(Array)"),
488        }
489    }
490
491    #[test]
492    fn test_object_empty() {
493        let table = json_test_table();
494        let val = serde_json::json!({}).to_value(&table).unwrap();
495        match &val {
496            Value::Con(id, fields) => {
497                assert_eq!(table.name_of(*id), Some("Object"));
498                assert_eq!(fields.len(), 1);
499                // Inner should be Tip (empty map)
500                match &fields[0] {
501                    Value::Con(tip_id, tip_fields) => {
502                        assert_eq!(table.name_of(*tip_id), Some("Tip"));
503                        assert!(tip_fields.is_empty());
504                    }
505                    _ => panic!("Expected Con(Tip)"),
506                }
507            }
508            _ => panic!("Expected Con(Object)"),
509        }
510    }
511
512    #[test]
513    fn test_object_single_key() {
514        let table = json_test_table();
515        let val = serde_json::json!({"a": 1}).to_value(&table).unwrap();
516        match &val {
517            Value::Con(id, fields) => {
518                assert_eq!(table.name_of(*id), Some("Object"));
519                // Inner should be Bin(size=1, key, value, Tip, Tip)
520                match &fields[0] {
521                    Value::Con(bin_id, bin_fields) => {
522                        assert_eq!(table.name_of(*bin_id), Some("Bin"));
523                        assert_eq!(bin_fields.len(), 5);
524                        // size field: I#(1)
525                        match &bin_fields[0] {
526                            Value::Con(i_id, i_fields) => {
527                                assert_eq!(table.name_of(*i_id), Some("I#"));
528                                match &i_fields[0] {
529                                    Value::Lit(Literal::LitInt(n)) => assert_eq!(*n, 1),
530                                    _ => panic!("Expected LitInt(1)"),
531                                }
532                            }
533                            _ => panic!("Expected Con(I#)"),
534                        }
535                        // left and right should be Tip
536                        match &bin_fields[3] {
537                            Value::Con(tip_id, _) => {
538                                assert_eq!(table.name_of(*tip_id), Some("Tip"));
539                            }
540                            _ => panic!("Expected Con(Tip)"),
541                        }
542                        match &bin_fields[4] {
543                            Value::Con(tip_id, _) => {
544                                assert_eq!(table.name_of(*tip_id), Some("Tip"));
545                            }
546                            _ => panic!("Expected Con(Tip)"),
547                        }
548                    }
549                    _ => panic!("Expected Con(Bin)"),
550                }
551            }
552            _ => panic!("Expected Con(Object)"),
553        }
554    }
555
556    #[test]
557    fn test_object_two_keys() {
558        let table = json_test_table();
559        let val = serde_json::json!({"a": 1, "b": 2})
560            .to_value(&table)
561            .unwrap();
562        match &val {
563            Value::Con(id, fields) => {
564                assert_eq!(table.name_of(*id), Some("Object"));
565                // Root should be Bin
566                match &fields[0] {
567                    Value::Con(bin_id, bin_fields) => {
568                        assert_eq!(table.name_of(*bin_id), Some("Bin"));
569                        assert_eq!(bin_fields.len(), 5);
570                        // size = 2
571                        match &bin_fields[0] {
572                            Value::Con(_, i_fields) => match &i_fields[0] {
573                                Value::Lit(Literal::LitInt(n)) => assert_eq!(*n, 2),
574                                _ => panic!("Expected LitInt(2)"),
575                            },
576                            _ => panic!("Expected I#"),
577                        }
578                    }
579                    _ => panic!("Expected Con(Bin)"),
580                }
581            }
582            _ => panic!("Expected Con(Object)"),
583        }
584    }
585
586    #[test]
587    fn test_object_three_keys_balanced() {
588        let table = json_test_table();
589        let val = serde_json::json!({"a": 1, "b": 2, "c": 3})
590            .to_value(&table)
591            .unwrap();
592        match &val {
593            Value::Con(id, fields) => {
594                assert_eq!(table.name_of(*id), Some("Object"));
595                // Root Bin should have non-Tip children (balanced tree)
596                match &fields[0] {
597                    Value::Con(bin_id, bin_fields) => {
598                        assert_eq!(table.name_of(*bin_id), Some("Bin"));
599                        // At least one child should be a Bin (not both Tip)
600                        let left_is_bin = matches!(&bin_fields[3], Value::Con(id, _) if table.name_of(*id) == Some("Bin"));
601                        let right_is_bin = matches!(&bin_fields[4], Value::Con(id, _) if table.name_of(*id) == Some("Bin"));
602                        assert!(
603                            left_is_bin || right_is_bin,
604                            "Expected at least one Bin child for 3-entry map"
605                        );
606                    }
607                    _ => panic!("Expected Con(Bin)"),
608                }
609            }
610            _ => panic!("Expected Con(Object)"),
611        }
612    }
613
614    #[test]
615    fn test_object_large() {
616        let table = json_test_table();
617        let mut map = serde_json::Map::new();
618        for i in 0..12 {
619            map.insert(format!("key_{}", i), serde_json::json!(i));
620        }
621        let json = serde_json::Value::Object(map);
622        let val = json.to_value(&table).unwrap();
623        match &val {
624            Value::Con(id, _) => assert_eq!(table.name_of(*id), Some("Object")),
625            _ => panic!("Expected Con(Object)"),
626        }
627    }
628
629    #[test]
630    fn test_deep_nesting() {
631        let table = json_test_table();
632        let json = serde_json::json!({"a": {"b": {"c": 1}}});
633        let val = json.to_value(&table).unwrap();
634        match &val {
635            Value::Con(id, fields) => {
636                assert_eq!(table.name_of(*id), Some("Object"));
637                // Drill into the map → key "a" → value is Object → key "b" → ...
638                // Just verify it doesn't panic and is Object
639                assert_eq!(fields.len(), 1);
640            }
641            _ => panic!("Expected Con(Object)"),
642        }
643    }
644
645    #[test]
646    fn test_mixed_types() {
647        let table = json_test_table();
648        let json = serde_json::json!({
649            "s": "str",
650            "n": 42,
651            "b": true,
652            "a": [1, 2],
653            "o": {"x": 1},
654            "null": null
655        });
656        let val = json.to_value(&table).unwrap();
657        match &val {
658            Value::Con(id, _) => assert_eq!(table.name_of(*id), Some("Object")),
659            _ => panic!("Expected Con(Object)"),
660        }
661    }
662
663    #[test]
664    fn test_ambiguous_tip_with_companion() {
665        // Simulate Data.Map and Data.Set both having Tip/0
666        let mut table = json_test_table();
667        // Add a second "Tip" with a far-away ID (simulating Data.Set.Tip)
668        table.insert(DataCon {
669            id: DataConId(500),
670            name: "Tip".into(),
671            tag: 1,
672            rep_arity: 0,
673            field_bangs: vec![],
674            qualified_name: None,
675        });
676        // keymap_to_value should still work because it uses get_companion
677        // to find the Tip closest to Bin
678        let json = serde_json::json!({"key": "value"});
679        let val = json.to_value(&table).unwrap();
680        match &val {
681            Value::Con(id, _) => assert_eq!(table.name_of(*id), Some("Object")),
682            _ => panic!("Expected Con(Object)"),
683        }
684    }
685
686    #[test]
687    fn test_qualified_name_resolves_ambiguous_map_constructors() {
688        // Build a table where Bin/Tip from Data.Map AND Data.Set are present
689        // with qualified names — the qualified path should pick the right ones.
690        let mut t = DataConTable::new();
691        let cons: &[(&str, u32, u32)] = &[
692            ("Object", 0, 1),
693            ("Array", 1, 1),
694            ("String", 2, 1),
695            ("Number", 3, 1),
696            ("Bool", 4, 1),
697            ("Null", 5, 0),
698            ("True", 8, 0),
699            ("False", 9, 0),
700            ("[]", 10, 0),
701            (":", 11, 2),
702            ("Text", 12, 3),
703            ("I#", 13, 1),
704        ];
705        for (i, (name, tag, arity)) in cons.iter().enumerate() {
706            t.insert(DataCon {
707                id: DataConId(i as u64),
708                name: (*name).into(),
709                tag: *tag,
710                rep_arity: *arity,
711                field_bangs: vec![],
712                qualified_name: None,
713            });
714        }
715        // Data.Map constructors with qualified names
716        t.insert(DataCon {
717            id: DataConId(100),
718            name: "Bin".into(),
719            tag: 1,
720            rep_arity: 5,
721            field_bangs: vec![],
722            qualified_name: Some("Data.Map.Bin".into()),
723        });
724        t.insert(DataCon {
725            id: DataConId(101),
726            name: "Tip".into(),
727            tag: 2,
728            rep_arity: 0,
729            field_bangs: vec![],
730            qualified_name: Some("Data.Map.Tip".into()),
731        });
732        // Data.Set constructors with SAME unqualified names
733        t.insert(DataCon {
734            id: DataConId(200),
735            name: "Bin".into(),
736            tag: 1,
737            rep_arity: 3,
738            field_bangs: vec![],
739            qualified_name: Some("Data.Set.Bin".into()),
740        });
741        t.insert(DataCon {
742            id: DataConId(201),
743            name: "Tip".into(),
744            tag: 2,
745            rep_arity: 0,
746            field_bangs: vec![],
747            qualified_name: Some("Data.Set.Tip".into()),
748        });
749
750        // keymap_to_value should resolve via qualified names
751        let json = serde_json::json!({"a": 1, "b": 2});
752        let val = json.to_value(&t).unwrap();
753        match &val {
754            Value::Con(id, fields) => {
755                assert_eq!(t.name_of(*id), Some("Object"));
756                // Inner map should use Data.Map.Bin (id=100), not Data.Set.Bin
757                match &fields[0] {
758                    Value::Con(bin_id, _) => {
759                        assert_eq!(*bin_id, DataConId(100));
760                    }
761                    _ => panic!("Expected Con(Bin)"),
762                }
763            }
764            _ => panic!("Expected Con(Object)"),
765        }
766    }
767}