Skip to main content

conjure_cp_cli/utils/
simplified_json.rs

1//! Conjure's simplified JSON format for parameters and solutions.
2//!
3//! Matches Conjure `--output-format=json`: integers and booleans are JSON scalars, sets and
4//! tuples are arrays, records are objects, and int-indexed matrices are objects whose keys are
5//! the printed index values. Non-int-indexed matrices encode as arrays.
6
7use std::collections::BTreeMap;
8use std::fmt::Write as _;
9
10use anyhow::{Context as _, anyhow, bail};
11use conjure_cp::Model;
12use conjure_cp::ast::{
13    AbstractLiteral, DeclarationPtr, DomainPtr, Field, GroundDomain, Literal, Moo, Name, Range,
14};
15use serde_json::{Map, Number, Value as JsonValue};
16
17use crate::utils::json::sort_json_object;
18
19/// Render solutions as a JSON array of assignment objects (Conjure `--solutions-in-one-file`).
20pub fn solutions_to_simplified_json(
21    solutions: &[BTreeMap<Name, Literal>],
22) -> anyhow::Result<JsonValue> {
23    let mut sorted = solutions.to_vec();
24    sorted.sort_by(solution_key_cmp);
25    let mut items = Vec::with_capacity(sorted.len());
26    for solution in &sorted {
27        items.push(solution_to_simplified_json(solution)?);
28    }
29    Ok(JsonValue::Array(items))
30}
31
32/// Pretty-print solutions in Conjure's simplified JSON layout.
33pub fn solutions_to_simplified_json_string(
34    solutions: &[BTreeMap<Name, Literal>],
35) -> anyhow::Result<String> {
36    let value = solutions_to_simplified_json(solutions)?;
37    Ok(format!("{}\n", serde_json::to_string_pretty(&value)?))
38}
39
40/// Parse a Conjure-style solutions JSON document (array, or a single object).
41pub fn solutions_from_simplified_json(
42    value: &JsonValue,
43    domains: &BTreeMap<Name, DomainPtr>,
44) -> anyhow::Result<Vec<BTreeMap<Name, Literal>>> {
45    match value {
46        JsonValue::Array(items) => items
47            .iter()
48            .map(|item| solution_from_simplified_json(item, domains))
49            .collect(),
50        JsonValue::Object(_) => Ok(vec![solution_from_simplified_json(value, domains)?]),
51        _ => bail!("expected a JSON array or object of solutions"),
52    }
53}
54
55/// Parse solutions JSON text.
56pub fn solutions_from_simplified_json_str(
57    text: &str,
58    domains: &BTreeMap<Name, DomainPtr>,
59) -> anyhow::Result<Vec<BTreeMap<Name, Literal>>> {
60    let value: JsonValue = serde_json::from_str(text).context("parsing solutions JSON")?;
61    solutions_from_simplified_json(&value, domains)
62}
63
64/// Render a single assignment as a simplified JSON object.
65pub fn solution_to_simplified_json(
66    solution: &BTreeMap<Name, Literal>,
67) -> anyhow::Result<JsonValue> {
68    let mut object = Map::new();
69    for (name, value) in solution {
70        object.insert(name.to_string(), literal_to_simplified_json(value)?);
71    }
72    Ok(sort_json_object(&JsonValue::Object(object), false))
73}
74
75/// Parse a single assignment object.
76pub fn solution_from_simplified_json(
77    value: &JsonValue,
78    domains: &BTreeMap<Name, DomainPtr>,
79) -> anyhow::Result<BTreeMap<Name, Literal>> {
80    let object = value
81        .as_object()
82        .ok_or_else(|| anyhow!("each solution must be a JSON object"))?;
83    let mut solution = BTreeMap::new();
84    for (name, json_value) in object {
85        let name = Name::user(name.as_str());
86        let domain = domains.get(&name);
87        let literal = literal_from_simplified_json(json_value, domain)
88            .with_context(|| format!("parsing value for `{name}`"))?;
89        solution.insert(name, literal);
90    }
91    Ok(solution)
92}
93
94/// Render a parameter file as a single simplified JSON object.
95pub fn params_to_simplified_json(params: &BTreeMap<Name, Literal>) -> anyhow::Result<JsonValue> {
96    solution_to_simplified_json(params)
97}
98
99/// Parse a parameter JSON object using given domains from the problem model.
100pub fn params_from_simplified_json(
101    value: &JsonValue,
102    given_domains: &BTreeMap<Name, DomainPtr>,
103) -> anyhow::Result<BTreeMap<Name, Literal>> {
104    solution_from_simplified_json(value, given_domains)
105}
106
107/// Parse parameter JSON text.
108pub fn params_from_simplified_json_str(
109    text: &str,
110    given_domains: &BTreeMap<Name, DomainPtr>,
111) -> anyhow::Result<BTreeMap<Name, Literal>> {
112    let value: JsonValue = serde_json::from_str(text).context("parsing parameter JSON")?;
113    params_from_simplified_json(&value, given_domains)
114}
115
116/// Encode a literal in Conjure's simplified JSON format.
117pub fn literal_to_simplified_json(literal: &Literal) -> anyhow::Result<JsonValue> {
118    match literal {
119        Literal::Unnamed(_) => bail!("simplified JSON for unnamed types is not supported yet"),
120        Literal::EnumVariant(_) => {
121            bail!("simplified JSON for enumerated types is not supported yet")
122        }
123        Literal::Bool(b) => Ok(JsonValue::Bool(*b)),
124        Literal::Int(i) => Ok(JsonValue::Number(Number::from(*i))),
125        Literal::AbstractLiteral(abs) => abstract_literal_to_simplified_json(abs),
126    }
127}
128
129fn abstract_literal_to_simplified_json(
130    abs: &AbstractLiteral<Literal>,
131) -> anyhow::Result<JsonValue> {
132    match abs {
133        AbstractLiteral::Set(elems) | AbstractLiteral::MSet(elems) => elems_as_json_array(elems),
134        AbstractLiteral::Tuple(elems) | AbstractLiteral::Sequence(elems) => {
135            elems_as_json_array(elems)
136        }
137        AbstractLiteral::Record(fields) => {
138            let mut object = Map::new();
139            for field in fields {
140                object.insert(
141                    field.name.to_string(),
142                    literal_to_simplified_json(&field.value)?,
143                );
144            }
145            Ok(JsonValue::Object(object))
146        }
147        AbstractLiteral::Variant(field) => {
148            let mut object = Map::new();
149            object.insert(
150                field.name.to_string(),
151                literal_to_simplified_json(&field.value)?,
152            );
153            Ok(JsonValue::Object(object))
154        }
155        AbstractLiteral::Matrix(elems, index_domain) => {
156            matrix_to_simplified_json(elems, index_domain.as_ref())
157        }
158        AbstractLiteral::Function(pairs) => function_to_simplified_json(pairs),
159        AbstractLiteral::Relation(rows) => {
160            let mut items = Vec::with_capacity(rows.len());
161            for row in rows {
162                items.push(elems_as_json_array(row)?);
163            }
164            Ok(JsonValue::Array(items))
165        }
166        AbstractLiteral::Partition(parts) => {
167            let mut items = Vec::with_capacity(parts.len());
168            for part in parts {
169                items.push(elems_as_json_array(part)?);
170            }
171            Ok(JsonValue::Array(items))
172        }
173        AbstractLiteral::Permutation(cycles) => {
174            let mut items = Vec::with_capacity(cycles.len());
175            for cycle in cycles {
176                items.push(elems_as_json_array(cycle)?);
177            }
178            Ok(JsonValue::Array(items))
179        }
180    }
181}
182
183fn matrix_to_simplified_json(
184    elems: &[Literal],
185    index_domain: &GroundDomain,
186) -> anyhow::Result<JsonValue> {
187    let GroundDomain::Int(_, _) = index_domain else {
188        return elems_as_json_array(elems);
189    };
190
191    let indices: Vec<Literal> = match index_domain.values() {
192        Ok(iter) => iter.collect(),
193        Err(_) => return elems_as_json_array(elems),
194    };
195
196    if indices.len() != elems.len() {
197        return elems_as_json_array(elems);
198    }
199
200    dictionary_from_pairs(indices.into_iter().zip(elems.iter().cloned()))
201}
202
203fn function_to_simplified_json(pairs: &[(Literal, Literal)]) -> anyhow::Result<JsonValue> {
204    dictionary_from_pairs(pairs.iter().cloned())
205}
206
207fn dictionary_from_pairs(
208    pairs: impl IntoIterator<Item = (Literal, Literal)>,
209) -> anyhow::Result<JsonValue> {
210    let mut object_entries = Vec::new();
211    let mut array_entries = Vec::new();
212    let mut all_keys_ok = true;
213
214    for (key, value) in pairs {
215        let key_json = literal_to_simplified_json(&key)?;
216        let value_json = literal_to_simplified_json(&value)?;
217        array_entries.push(JsonValue::Array(vec![key_json.clone(), value_json.clone()]));
218        match &key_json {
219            JsonValue::Bool(_) | JsonValue::Number(_) | JsonValue::String(_) => {
220                object_entries.push((json_key_string(&key_json)?, value_json));
221            }
222            _ => {
223                all_keys_ok = false;
224            }
225        }
226    }
227
228    if all_keys_ok {
229        Ok(JsonValue::Object(object_entries.into_iter().collect()))
230    } else {
231        Ok(JsonValue::Array(array_entries))
232    }
233}
234
235fn elems_as_json_array(elems: &[Literal]) -> anyhow::Result<JsonValue> {
236    let mut items = Vec::with_capacity(elems.len());
237    for elem in elems {
238        items.push(literal_to_simplified_json(elem)?);
239    }
240    Ok(JsonValue::Array(items))
241}
242
243fn json_key_string(value: &JsonValue) -> anyhow::Result<String> {
244    match value {
245        JsonValue::Bool(b) => Ok(b.to_string()),
246        JsonValue::Number(n) => Ok(n.to_string()),
247        JsonValue::String(s) => Ok(s.clone()),
248        _ => bail!("JSON object keys must be bool, number, or string"),
249    }
250}
251
252/// Decode a simplified JSON value, using `domain` to disambiguate arrays and objects.
253pub fn literal_from_simplified_json(
254    value: &JsonValue,
255    domain: Option<&DomainPtr>,
256) -> anyhow::Result<Literal> {
257    if let Some(domain) = domain {
258        if let Some(ground) = domain.as_ground() {
259            return literal_from_simplified_json_with_ground(value, ground);
260        }
261        // Domain lettings (e.g. `find x : R`) are unresolved until resolved through the symbol
262        // table; resolve them so records/sets/etc. are not misread as matrices.
263        if let Ok(ground) = domain.resolve() {
264            return literal_from_simplified_json_with_ground(value, ground.as_ref());
265        }
266    }
267    literal_from_simplified_json_unguided(value)
268}
269
270fn literal_from_simplified_json_with_ground(
271    value: &JsonValue,
272    domain: &GroundDomain,
273) -> anyhow::Result<Literal> {
274    match domain {
275        GroundDomain::EnumeratedType(_, _) | GroundDomain::UnnamedType(_) => {
276            bail!("simplified JSON for enumerated and unnamed types is not supported yet")
277        }
278        GroundDomain::Empty(_) => bail!("cannot parse a value for an empty domain"),
279        GroundDomain::Bool => match value {
280            JsonValue::Bool(b) => Ok(Literal::Bool(*b)),
281            JsonValue::Number(n) => match n.as_i64() {
282                Some(1) => Ok(Literal::Bool(true)),
283                Some(0) => Ok(Literal::Bool(false)),
284                _ => bail!("expected a boolean"),
285            },
286            _ => bail!("expected a boolean"),
287        },
288        GroundDomain::Int(_, _) => Ok(Literal::Int(json_to_i32(value)?)),
289        GroundDomain::Tuple(inners, _) => {
290            let JsonValue::Array(items) = value else {
291                bail!("expected a JSON array for a tuple");
292            };
293            if items.len() != inners.len() {
294                bail!(
295                    "tuple arity mismatch: expected {}, got {}",
296                    inners.len(),
297                    items.len()
298                );
299            }
300            let mut elems = Vec::with_capacity(items.len());
301            for (item, inner) in items.iter().zip(inners) {
302                elems.push(literal_from_simplified_json_with_ground(item, inner)?);
303            }
304            Ok(Literal::AbstractLiteral(AbstractLiteral::Tuple(elems)))
305        }
306        GroundDomain::Record(fields, _) => {
307            let JsonValue::Object(object) = value else {
308                bail!("expected a JSON object for a record");
309            };
310            let mut entries = Vec::with_capacity(fields.len());
311            for field in fields {
312                let key = field.name.to_string();
313                let Some(item) = object.get(&key) else {
314                    bail!("missing record field `{key}`");
315                };
316                entries.push(Field {
317                    name: field.name.clone(),
318                    value: literal_from_simplified_json_with_ground(item, field.value.as_ref())?,
319                });
320            }
321            Ok(Literal::AbstractLiteral(AbstractLiteral::Record(entries)))
322        }
323        GroundDomain::Variant(fields, _) => {
324            let JsonValue::Object(object) = value else {
325                bail!("expected a JSON object for a variant");
326            };
327            if object.len() != 1 {
328                bail!(
329                    "variant value must contain exactly one alternative, got {}",
330                    object.len()
331                );
332            }
333            let (key, item) = object.iter().next().expect("object has one entry");
334            let field = fields
335                .iter()
336                .find(|field| field.name.to_string() == *key)
337                .ok_or_else(|| anyhow!("unknown variant alternative `{key}`"))?;
338            Ok(Literal::AbstractLiteral(AbstractLiteral::Variant(
339                Moo::new(Field {
340                    name: field.name.clone(),
341                    value: literal_from_simplified_json_with_ground(item, field.value.as_ref())?,
342                }),
343            )))
344        }
345        GroundDomain::Matrix(inner, index_domains, _) => {
346            matrix_from_simplified_json(value, inner.as_ref(), index_domains)
347        }
348        GroundDomain::Sequence(_, inner) => match value {
349            JsonValue::Array(items) => {
350                let mut elems = Vec::with_capacity(items.len());
351                for item in items {
352                    elems.push(literal_from_simplified_json_with_ground(item, inner)?);
353                }
354                Ok(Literal::AbstractLiteral(AbstractLiteral::Sequence(elems)))
355            }
356            JsonValue::Object(object) => sequence_from_object(object, inner),
357            _ => bail!("expected a JSON array for a sequence"),
358        },
359        GroundDomain::Set(_, inner) => {
360            let JsonValue::Array(items) = value else {
361                bail!("expected a JSON array for a set");
362            };
363            let mut elems = Vec::with_capacity(items.len());
364            for item in items {
365                elems.push(literal_from_simplified_json_with_ground(item, inner)?);
366            }
367            Ok(Literal::AbstractLiteral(AbstractLiteral::Set(elems)))
368        }
369        GroundDomain::MSet(_, inner) => {
370            let JsonValue::Array(items) = value else {
371                bail!("expected a JSON array for a multiset");
372            };
373            let mut elems = Vec::with_capacity(items.len());
374            for item in items {
375                elems.push(literal_from_simplified_json_with_ground(item, inner)?);
376            }
377            Ok(Literal::AbstractLiteral(AbstractLiteral::MSet(elems)))
378        }
379        GroundDomain::Function(_, from, to) => {
380            function_from_simplified_json(value, from.as_ref(), to.as_ref())
381        }
382        GroundDomain::Relation(_, inner_doms) => relation_from_simplified_json(value, inner_doms),
383        GroundDomain::Partition(_, inner) => partition_from_simplified_json(value, inner),
384        GroundDomain::Permutation(_, inner) => permutation_from_simplified_json(value, inner),
385    }
386}
387
388fn partition_from_simplified_json(
389    value: &JsonValue,
390    inner: &GroundDomain,
391) -> anyhow::Result<Literal> {
392    let JsonValue::Array(items) = value else {
393        bail!("expected a JSON array for a partition");
394    };
395    let mut parts = Vec::with_capacity(items.len());
396    for item in items {
397        let JsonValue::Array(elems) = item else {
398            bail!("expected a JSON array for a partition part");
399        };
400        let mut part = Vec::with_capacity(elems.len());
401        for elem in elems {
402            part.push(literal_from_simplified_json_with_ground(elem, inner)?);
403        }
404        parts.push(part);
405    }
406    Ok(Literal::AbstractLiteral(AbstractLiteral::Partition(parts)))
407}
408
409fn permutation_from_simplified_json(
410    value: &JsonValue,
411    inner: &GroundDomain,
412) -> anyhow::Result<Literal> {
413    let JsonValue::Array(items) = value else {
414        bail!("expected a JSON array for a permutation");
415    };
416    let mut cycles = Vec::with_capacity(items.len());
417    for item in items {
418        let JsonValue::Array(elems) = item else {
419            bail!("expected a JSON array for a permutation cycle");
420        };
421        let mut cycle = Vec::with_capacity(elems.len());
422        for elem in elems {
423            cycle.push(literal_from_simplified_json_with_ground(elem, inner)?);
424        }
425        cycles.push(cycle);
426    }
427    Ok(Literal::AbstractLiteral(AbstractLiteral::Permutation(
428        cycles,
429    )))
430}
431
432fn relation_from_simplified_json(
433    value: &JsonValue,
434    inner_doms: &[Moo<GroundDomain>],
435) -> anyhow::Result<Literal> {
436    let JsonValue::Array(items) = value else {
437        bail!("expected a JSON array for a relation");
438    };
439    let mut tuples = Vec::with_capacity(items.len());
440    for item in items {
441        let JsonValue::Array(fields) = item else {
442            bail!("expected a JSON array for a relation tuple");
443        };
444        if fields.len() != inner_doms.len() {
445            bail!(
446                "relation tuple arity mismatch: expected {}, got {}",
447                inner_doms.len(),
448                fields.len()
449            );
450        }
451        let mut tuple = Vec::with_capacity(fields.len());
452        for (field, dom) in fields.iter().zip(inner_doms) {
453            tuple.push(literal_from_simplified_json_with_ground(field, dom)?);
454        }
455        tuples.push(tuple);
456    }
457    Ok(Literal::AbstractLiteral(AbstractLiteral::Relation(tuples)))
458}
459
460fn matrix_from_simplified_json(
461    value: &JsonValue,
462    inner: &GroundDomain,
463    index_domains: &[Moo<GroundDomain>],
464) -> anyhow::Result<Literal> {
465    let (first_index, rest) = index_domains
466        .split_first()
467        .ok_or_else(|| anyhow!("matrix domain has no index domains"))?;
468
469    let elem_domain: GroundDomain = if rest.is_empty() {
470        inner.clone()
471    } else {
472        GroundDomain::Matrix(Moo::new(inner.clone()), rest.to_vec(), None)
473    };
474
475    match value {
476        JsonValue::Object(object) => {
477            let mut pairs = Vec::with_capacity(object.len());
478            for (key, item) in object {
479                let index = index_key_to_literal(key, first_index)?;
480                let elem = literal_from_simplified_json_with_ground(item, &elem_domain)?;
481                pairs.push((index, elem));
482            }
483            pairs.sort_by(|a, b| a.0.essence_cmp(&b.0));
484            let keys: Vec<i32> = object
485                .keys()
486                .map(|k| k.parse::<i32>())
487                .collect::<Result<Vec<_>, _>>()
488                .unwrap_or_default();
489            let elems: Vec<Literal> = pairs.into_iter().map(|(_, v)| v).collect();
490            let index_domain = if let Ok(expected) = first_index.values() {
491                let expected: Vec<_> = expected.collect();
492                if expected.len() == elems.len() {
493                    first_index.as_ref().clone()
494                } else {
495                    infer_int_index_domain(&keys)
496                }
497            } else {
498                infer_int_index_domain(&keys)
499            };
500            Ok(Literal::AbstractLiteral(AbstractLiteral::Matrix(
501                elems,
502                index_domain.into(),
503            )))
504        }
505        JsonValue::Array(items) => {
506            let mut elems = Vec::with_capacity(items.len());
507            for item in items {
508                elems.push(literal_from_simplified_json_with_ground(
509                    item,
510                    &elem_domain,
511                )?);
512            }
513            let n = i32::try_from(elems.len()).context("matrix too large")?;
514            let index_domain = GroundDomain::Int(vec![Range::Bounded(1, n)], None);
515            Ok(Literal::AbstractLiteral(AbstractLiteral::Matrix(
516                elems,
517                index_domain.into(),
518            )))
519        }
520        _ => bail!("expected a JSON object or array for a matrix"),
521    }
522}
523
524fn infer_int_index_domain(keys: &[i32]) -> GroundDomain {
525    if keys.is_empty() {
526        return GroundDomain::Int(vec![], None);
527    }
528    let mut ints = keys.to_vec();
529    ints.sort_unstable();
530    let min = ints[0];
531    let max = *ints.last().expect("non-empty");
532    if max - min + 1 == ints.len() as i32 {
533        GroundDomain::Int(vec![Range::Bounded(min, max)], None)
534    } else {
535        GroundDomain::Int(ints.into_iter().map(Range::Single).collect(), None)
536    }
537}
538
539fn index_key_to_literal(key: &str, index_domain: &GroundDomain) -> anyhow::Result<Literal> {
540    match index_domain {
541        GroundDomain::Bool => match key {
542            "false" => Ok(Literal::Bool(false)),
543            "true" => Ok(Literal::Bool(true)),
544            _ => bail!("expected boolean matrix index key"),
545        },
546        _ => Ok(Literal::Int(key.parse::<i32>().with_context(|| {
547            format!("expected integer matrix index key, got `{key}`")
548        })?)),
549    }
550}
551
552fn sequence_from_object(
553    object: &Map<String, JsonValue>,
554    inner: &GroundDomain,
555) -> anyhow::Result<Literal> {
556    let mut pairs = Vec::with_capacity(object.len());
557    for (key, item) in object {
558        let index: i32 = key
559            .parse()
560            .with_context(|| format!("sequence index `{key}` is not an integer"))?;
561        pairs.push((
562            index,
563            literal_from_simplified_json_with_ground(item, inner)?,
564        ));
565    }
566    pairs.sort_by_key(|(i, _)| *i);
567    Ok(Literal::AbstractLiteral(AbstractLiteral::Sequence(
568        pairs.into_iter().map(|(_, v)| v).collect(),
569    )))
570}
571
572fn function_from_simplified_json(
573    value: &JsonValue,
574    from: &GroundDomain,
575    to: &GroundDomain,
576) -> anyhow::Result<Literal> {
577    match value {
578        JsonValue::Object(object) => {
579            let mut pairs = Vec::with_capacity(object.len());
580            for (key, item) in object {
581                let domain_key = index_key_to_literal(key, from)?;
582                let mapped = literal_from_simplified_json_with_ground(item, to)?;
583                pairs.push((domain_key, mapped));
584            }
585            Ok(Literal::AbstractLiteral(AbstractLiteral::Function(pairs)))
586        }
587        JsonValue::Array(items) => {
588            let mut pairs = Vec::with_capacity(items.len());
589            for item in items {
590                let JsonValue::Array(pair) = item else {
591                    bail!("function array entries must be [from, to] pairs");
592                };
593                if pair.len() != 2 {
594                    bail!("function array entries must be [from, to] pairs");
595                }
596                pairs.push((
597                    literal_from_simplified_json_with_ground(&pair[0], from)?,
598                    literal_from_simplified_json_with_ground(&pair[1], to)?,
599                ));
600            }
601            Ok(Literal::AbstractLiteral(AbstractLiteral::Function(pairs)))
602        }
603        _ => bail!("expected a JSON object or array for a function"),
604    }
605}
606
607fn literal_from_simplified_json_unguided(value: &JsonValue) -> anyhow::Result<Literal> {
608    match value {
609        JsonValue::Bool(b) => Ok(Literal::Bool(*b)),
610        JsonValue::Number(_) | JsonValue::String(_) => Ok(Literal::Int(json_to_i32(value)?)),
611        JsonValue::Array(items) => {
612            let mut elems = Vec::with_capacity(items.len());
613            for item in items {
614                elems.push(literal_from_simplified_json_unguided(item)?);
615            }
616            Ok(Literal::AbstractLiteral(AbstractLiteral::Set(elems)))
617        }
618        JsonValue::Object(object) => {
619            let all_int_keys = object.keys().all(|key| key.parse::<i32>().is_ok());
620            if all_int_keys {
621                let mut pairs = Vec::with_capacity(object.len());
622                for (key, item) in object {
623                    let index: i32 = key.parse().with_context(|| {
624                        format!("unguided object key `{key}` is not an integer")
625                    })?;
626                    pairs.push((index, literal_from_simplified_json_unguided(item)?));
627                }
628                pairs.sort_by_key(|(i, _)| *i);
629                let keys: Vec<i32> = pairs.iter().map(|(i, _)| *i).collect();
630                let elems: Vec<_> = pairs.into_iter().map(|(_, v)| v).collect();
631                return Ok(Literal::AbstractLiteral(AbstractLiteral::Matrix(
632                    elems,
633                    infer_int_index_domain(&keys).into(),
634                )));
635            }
636
637            // Non-integer keys: treat as a record (Conjure simplified JSON).
638            let mut entries = Vec::with_capacity(object.len());
639            for (key, item) in object {
640                entries.push(Field {
641                    name: Name::user(key.as_str()),
642                    value: literal_from_simplified_json_unguided(item)?,
643                });
644            }
645            entries.sort_by(|a, b| a.name.cmp(&b.name));
646            Ok(Literal::AbstractLiteral(AbstractLiteral::Record(entries)))
647        }
648        JsonValue::Null => bail!("null is not a valid Essence literal"),
649    }
650}
651
652fn json_to_i32(value: &JsonValue) -> anyhow::Result<i32> {
653    match value {
654        JsonValue::Number(n) => n
655            .as_i64()
656            .and_then(|i| i32::try_from(i).ok())
657            .ok_or_else(|| anyhow!("expected a 32-bit integer")),
658        JsonValue::String(s) => s
659            .parse::<i32>()
660            .with_context(|| format!("expected an integer string, got `{s}`")),
661        _ => bail!("expected an integer"),
662    }
663}
664
665fn solution_key_cmp(
666    lhs: &BTreeMap<Name, Literal>,
667    rhs: &BTreeMap<Name, Literal>,
668) -> std::cmp::Ordering {
669    lhs.iter()
670        .zip(rhs)
671        .find_map(|((lhs_name, lhs_value), (rhs_name, rhs_value))| {
672            let ordering = lhs_name.cmp(rhs_name);
673            (ordering != std::cmp::Ordering::Equal)
674                .then_some(ordering)
675                .or_else(|| {
676                    let ordering = lhs_value.essence_cmp(rhs_value);
677                    (ordering != std::cmp::Ordering::Equal).then_some(ordering)
678                })
679        })
680        .unwrap_or_else(|| lhs.len().cmp(&rhs.len()))
681}
682
683/// Collect find/given domains from a model for JSON parsing guidance.
684pub fn domains_from_model(model: &Model) -> BTreeMap<Name, DomainPtr> {
685    let mut domains = BTreeMap::new();
686    for (name, decl) in model.symbols().clone().into_iter() {
687        if let Some(domain) = decl.domain() {
688            domains.insert(name, domain);
689        }
690    }
691    domains
692}
693
694/// Build a parameter model of value-lettings from simplified JSON assignments.
695pub fn param_model_from_assignments(
696    params: BTreeMap<Name, Literal>,
697    given_domains: &BTreeMap<Name, DomainPtr>,
698    context: std::sync::Arc<std::sync::RwLock<conjure_cp::context::Context<'static>>>,
699) -> Model {
700    let mut model = Model::new(context);
701    for (name, literal) in params {
702        let decl = match given_domains.get(&name) {
703            Some(domain) => {
704                DeclarationPtr::new_value_letting_with_domain(name, literal.into(), domain.clone())
705            }
706            None => DeclarationPtr::new_value_letting(name, literal.into()),
707        };
708        model.symbols_mut().update_insert(decl);
709    }
710    model
711}
712
713/// Render pretty JSON with stable key order for golden comparisons.
714pub fn canonical_simplified_json_string(value: &JsonValue) -> anyhow::Result<String> {
715    let sorted = sort_json_object(value, true);
716    Ok(format!("{}\n", serde_json::to_string_pretty(&sorted)?))
717}
718
719/// Append pretty JSON to a string buffer.
720pub fn write_simplified_json(value: &JsonValue, out: &mut String) -> anyhow::Result<()> {
721    write!(out, "{}", serde_json::to_string_pretty(value)?)?;
722    out.push('\n');
723    Ok(())
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729    use conjure_cp::ast::{Domain, SetAttr};
730
731    fn int_dom(lo: i32, hi: i32) -> DomainPtr {
732        Domain::int(vec![Range::Bounded(lo, hi)])
733    }
734
735    #[test]
736    fn round_trips_scalars_and_set() {
737        let mut domains = BTreeMap::new();
738        domains.insert(Name::user("x"), int_dom(1, 3));
739        domains.insert(
740            Name::user("s"),
741            Domain::set(SetAttr::<i32>::default(), int_dom(1, 3)),
742        );
743
744        let json = serde_json::json!([{"s": [1, 3], "x": 2}]);
745        let solutions = solutions_from_simplified_json(&json, &domains).unwrap();
746        assert_eq!(solutions.len(), 1);
747        assert_eq!(solutions[0].get(&Name::user("x")), Some(&Literal::Int(2)));
748        let Literal::AbstractLiteral(AbstractLiteral::Set(elems)) =
749            solutions[0].get(&Name::user("s")).unwrap()
750        else {
751            panic!("expected set");
752        };
753        assert_eq!(elems, &vec![Literal::Int(1), Literal::Int(3)]);
754
755        let rendered = solutions_to_simplified_json(&solutions).unwrap();
756        let again = solutions_from_simplified_json(&rendered, &domains).unwrap();
757        assert_eq!(again, solutions);
758    }
759
760    #[test]
761    fn round_trips_int_indexed_matrix() {
762        let matrix_dom = Domain::matrix(int_dom(1, 3), vec![int_dom(1, 2)]);
763        let mut domains = BTreeMap::new();
764        domains.insert(Name::user("m"), matrix_dom);
765
766        let json = serde_json::json!([{"m": {"1": 1, "2": 2}}]);
767        let solutions = solutions_from_simplified_json(&json, &domains).unwrap();
768        let rendered = solutions_to_simplified_json(&solutions).unwrap();
769        let again = solutions_from_simplified_json(&rendered, &domains).unwrap();
770        assert_eq!(again, solutions);
771    }
772
773    #[test]
774    fn guided_variant_object_parses_as_variant() {
775        let mut domains = BTreeMap::new();
776        domains.insert(
777            Name::user("x"),
778            Domain::variant(vec![
779                Field {
780                    name: Name::user("flag"),
781                    value: Domain::bool(),
782                },
783                Field {
784                    name: Name::user("value"),
785                    value: int_dom(1, 3),
786                },
787            ]),
788        );
789
790        let json = serde_json::json!([{"x": {"value": 2}}]);
791        let solutions = solutions_from_simplified_json(&json, &domains).unwrap();
792        let expected = Literal::AbstractLiteral(AbstractLiteral::Variant(Moo::new(Field {
793            name: Name::user("value"),
794            value: Literal::Int(2),
795        })));
796        assert_eq!(solutions[0].get(&Name::user("x")), Some(&expected));
797
798        let rendered = solutions_to_simplified_json(&solutions).unwrap();
799        assert_eq!(
800            solutions_from_simplified_json(&rendered, &domains).unwrap(),
801            solutions
802        );
803    }
804
805    #[test]
806    fn unguided_object_with_named_fields_parses_as_record() {
807        let json = serde_json::json!({"a": true, "b": 3});
808        let literal = literal_from_simplified_json(&json, None).unwrap();
809        let Literal::AbstractLiteral(AbstractLiteral::Record(fields)) = literal else {
810            panic!("expected record, got {literal:?}");
811        };
812        assert_eq!(fields.len(), 2);
813        assert_eq!(fields[0].name, Name::user("a"));
814        assert_eq!(fields[0].value, Literal::Bool(true));
815        assert_eq!(fields[1].name, Name::user("b"));
816        assert_eq!(fields[1].value, Literal::Int(3));
817    }
818}