Skip to main content

camxes_rs/
jbo_tree.rs

1//! Graph-based output from [JboTree.hs](../JboTree.hs).
2//!
3//! Ports the proposition-to-graph conversion used by the Haskell JSON graph output.
4
5use crate::jbo_prop::{DecoratedTagUnit, JboModalOp, JboQuantifier, JboRel, JboTag, JboTagUnit, JboTerm, Texticule};
6use crate::logic::Prop;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10pub type JboProp = Prop<JboRel, JboTerm, String, JboModalOp, JboQuantifier>;
11
12// Ported from: JboTree.hs :: GraphOutput
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct GraphOutput {
15    pub format: String,
16    pub nodes: Vec<GraphNode>,
17    pub edges: Vec<GraphEdge>,
18}
19
20// Ported from: JboTree.hs :: GraphNode
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct GraphNode {
23    pub id: String,
24    #[serde(rename = "type")]
25    pub node_type: String,
26    pub data: NodeData,
27}
28
29// Ported from: JboTree.hs :: NodeData
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(tag = "kind")]
32pub enum NodeData {
33    #[serde(rename = "relation")]
34    Relation {
35        name: String,
36        #[serde(rename = "type")]
37        rel_type: String,
38        selmaho: Option<String>,
39    },
40    #[serde(rename = "term")]
41    Term {
42        value: String,
43        #[serde(rename = "type")]
44        term_type: String,
45        selmaho: Option<String>,
46    },
47    #[serde(rename = "quantifier")]
48    Quantifier {
49        quantifier: String,
50        variable: String,
51        selmaho: Option<String>,
52    },
53    #[serde(rename = "modal")]
54    Modal {
55        modal_type: String,
56        tag: Option<String>,
57        selmaho: Option<String>,
58    },
59    #[serde(rename = "connective")]
60    Connective {
61        connective: String,
62        selmaho: Option<String>,
63    },
64    #[serde(rename = "not")]
65    Not {
66        selmaho: Option<String>,
67    },
68    #[serde(rename = "side")]
69    SideTexticule {
70        side_type: String,
71        content: String,
72    },
73    #[serde(rename = "eet")]
74    Eet,
75    #[serde(rename = "error")]
76    Error {
77        message: String,
78    },
79}
80
81// Ported from: JboTree.hs :: GraphEdge
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct GraphEdge {
84    pub source: String,
85    pub target: String,
86    pub label: String,
87}
88
89// Ported from: JboTree.hs :: GraphState
90struct GraphState {
91    node_map: HashMap<String, String>,
92    nodes: Vec<GraphNode>,
93    edges: Vec<GraphEdge>,
94    next_id: usize,
95}
96
97impl GraphState {
98    fn new() -> Self {
99        GraphState {
100            node_map: HashMap::new(),
101            nodes: Vec::new(),
102            edges: Vec::new(),
103            next_id: 0,
104        }
105    }
106}
107
108// Ported from: JboTree.hs :: simpleHash
109fn simple_hash(s: &str) -> i32 {
110    let mut h: i32 = 2166136261u32 as i32;
111    for c in s.chars() {
112        h = h.wrapping_mul(16777619).wrapping_add(c as i32);
113    }
114    h
115}
116
117// Ported from: JboTree.hs :: contentHash
118fn content_hash(s: &str) -> String {
119    format!("h{}", simple_hash(s).abs())
120}
121
122// Ported from: JboTree.hs :: getOrCreateNode
123fn get_or_create_node(
124    state: &mut GraphState,
125    content_key: &str,
126    node_type: &str,
127    node_data: NodeData,
128) -> String {
129    if let Some(node_id) = state.node_map.get(content_key) {
130        return node_id.clone();
131    }
132
133    let node_id = format!("n{}", state.next_id);
134    state.next_id += 1;
135
136    state.nodes.push(GraphNode {
137        id: node_id.clone(),
138        node_type: node_type.to_string(),
139        data: node_data,
140    });
141    state.node_map.insert(content_key.to_string(), node_id.clone());
142
143    node_id
144}
145
146// Ported from: JboTree.hs :: addEdge
147fn add_edge(state: &mut GraphState, source: String, target: String, label: String) {
148    state.edges.push(GraphEdge { source, target, label });
149}
150
151// Ported from: JboTree.hs :: convertPropToGraphWithLastTerm / convertPropToGraph
152fn convert_prop_to_graph_with_last_term(
153    state: &mut GraphState,
154    prop: &JboProp,
155    parent_id: Option<String>,
156) -> (String, Option<String>) {
157    match prop {
158        Prop::Not(p) => {
159            let node_id = get_or_create_node(
160                state,
161                "not",
162                "not",
163                NodeData::Not { selmaho: Some("NA".to_string()) },
164            );
165            let (child_id, _) = convert_prop_to_graph_with_last_term(state, p, Some(node_id.clone()));
166            add_edge(state, node_id.clone(), child_id, String::new());
167            add_parent_edge(state, parent_id, &node_id);
168            (node_id, None)
169        }
170        Prop::Connected(c, p1, p2) => {
171            let conn = c.to_string();
172            let node_id = get_or_create_node(
173                state,
174                &format!("conn:{conn}"),
175                "connective",
176                NodeData::Connective { connective: conn, selmaho: Some("JOI".to_string()) },
177            );
178            let (left_id, _) = convert_prop_to_graph_with_last_term(state, p1, Some(node_id.clone()));
179            let (right_id, last_term) = convert_prop_to_graph_with_last_term(state, p2, Some(node_id.clone()));
180            add_edge(state, node_id.clone(), left_id, "L".to_string());
181            add_edge(state, node_id.clone(), right_id, "R".to_string());
182            add_parent_edge(state, parent_id, &node_id);
183            (node_id, last_term)
184        }
185        Prop::NonLogConnected(c, p1, p2) => {
186            let conn = c.clone();
187            let node_id = get_or_create_node(
188                state,
189                &format!("nlconn:{conn}"),
190                "non-log-connective",
191                NodeData::Connective { connective: conn, selmaho: Some("JOI".to_string()) },
192            );
193            let (left_id, _) = convert_prop_to_graph_with_last_term(state, p1, Some(node_id.clone()));
194            let (right_id, last_term) = convert_prop_to_graph_with_last_term(state, p2, Some(node_id.clone()));
195            add_edge(state, node_id.clone(), left_id, "L".to_string());
196            add_edge(state, node_id.clone(), right_id, "R".to_string());
197            add_parent_edge(state, parent_id, &node_id);
198            (node_id, last_term)
199        }
200        Prop::Quantified(q, restriction, body) => {
201            let n = state.next_id as i32;
202            let var_name = format!("x_{n}");
203            let q_name = convert_quantifier(q);
204            let node_id = get_or_create_node(
205                state,
206                &format!("quant:{q_name}:{var_name}"),
207                "quantifier",
208                NodeData::Quantifier {
209                    quantifier: q_name,
210                    variable: var_name,
211                    selmaho: Some("PA".to_string()),
212                },
213            );
214            if let Some(restriction) = restriction {
215                let restriction_prop = restriction(n);
216                let (restriction_id, _) = convert_prop_to_graph_with_last_term(state, &restriction_prop, Some(node_id.clone()));
217                add_edge(state, node_id.clone(), restriction_id, "restr".to_string());
218            }
219            let body_prop = body(n);
220            let (body_id, last_term) = convert_prop_to_graph_with_last_term(state, &body_prop, Some(node_id.clone()));
221            add_edge(state, node_id.clone(), body_id, String::new());
222            add_parent_edge(state, parent_id, &node_id);
223            (node_id, last_term)
224        }
225        Prop::Modal(modal, p) => {
226            let (modal_type, tag, term_id, selmaho) = convert_modal(state, modal);
227            let node_id = get_or_create_node(
228                state,
229                &format!("modal:{modal_type}:{tag:?}"),
230                "modal",
231                NodeData::Modal { modal_type, tag, selmaho },
232            );
233            if let Some(term_id) = term_id {
234                add_edge(state, node_id.clone(), term_id, "term".to_string());
235            }
236            let (child_id, last_term) = convert_prop_to_graph_with_last_term(state, p, Some(node_id.clone()));
237            add_edge(state, node_id.clone(), child_id, String::new());
238            add_parent_edge(state, parent_id, &node_id);
239            (node_id, last_term)
240        }
241        Prop::Rel(rel, terms) => {
242            let (node_id, last_term) = convert_rel_prop_to_graph(state, rel, terms, parent_id);
243            (node_id, last_term)
244        }
245        Prop::Eet => {
246            let node_id = get_or_create_node(state, "eet", "eet", NodeData::Eet);
247            add_parent_edge(state, parent_id, &node_id);
248            (node_id, None)
249        }
250    }
251}
252
253// Ported from: JboTree.hs :: convertPropToGraph
254fn convert_prop_to_graph(state: &mut GraphState, prop: &JboProp, parent_id: Option<String>) -> String {
255    convert_prop_to_graph_with_last_term(state, prop, parent_id).0
256}
257
258// Ported from: JboTree.hs :: addParentEdge
259fn add_parent_edge(state: &mut GraphState, parent_id: Option<String>, node_id: &str) {
260    if let Some(parent_id) = parent_id {
261        add_edge(state, parent_id, node_id.to_string(), String::new());
262    }
263}
264
265// Ported from: JboTree.hs :: convertRelPropToGraph
266fn convert_rel_prop_to_graph(
267    state: &mut GraphState,
268    rel: &JboRel,
269    terms: &[JboTerm],
270    parent_id: Option<String>,
271) -> (String, Option<String>) {
272    match rel {
273        JboRel::AbsPred(abstractor, pred) => {
274            let node_id = get_or_create_node(
275                state,
276                &format!("abs:{abstractor}"),
277                "abstraction",
278                NodeData::Relation {
279                    name: abstractor.clone(),
280                    rel_type: "abstraction".to_string(),
281                    selmaho: Some("NU".to_string()),
282                },
283            );
284            let dummy_args = vec![JboTerm::Unfilled; pred.arity];
285            let inner_prop = (pred.pred)(&dummy_args);
286            let (inner_id, _) = convert_prop_to_graph_with_last_term(state, &inner_prop, Some(node_id.clone()));
287            add_edge(state, node_id.clone(), inner_id, "body".to_string());
288            let last_term = add_term_edges(state, &node_id, terms, "x");
289            add_parent_edge(state, parent_id, &node_id);
290            (node_id, last_term)
291        }
292        JboRel::AbsProp(abstractor, inner_prop) => {
293            let node_id = get_or_create_node(
294                state,
295                &format!("absprop:{abstractor}"),
296                "abstraction-prop",
297                NodeData::Relation {
298                    name: abstractor.clone(),
299                    rel_type: "abstraction".to_string(),
300                    selmaho: Some("NU".to_string()),
301                },
302            );
303            let (inner_id, _) = convert_prop_to_graph_with_last_term(state, inner_prop, Some(node_id.clone()));
304            add_edge(state, node_id.clone(), inner_id, "body".to_string());
305            let last_term = add_term_edges(state, &node_id, terms, "x");
306            add_parent_edge(state, parent_id, &node_id);
307            (node_id, last_term)
308        }
309        _ => {
310            let (name, rel_type, selmaho) = convert_rel(rel);
311            let node_id = get_or_create_node(
312                state,
313                &format!("rel:{rel_type}:{name}"),
314                "relation",
315                NodeData::Relation { name, rel_type, selmaho },
316            );
317            let last_term = add_term_edges(state, &node_id, terms, "x");
318            add_parent_edge(state, parent_id, &node_id);
319            (node_id, last_term)
320        }
321    }
322}
323
324// Ported from: JboTree.hs :: addTermEdges
325fn add_term_edges(state: &mut GraphState, node_id: &str, terms: &[JboTerm], prefix: &str) -> Option<String> {
326    let mut last = None;
327    for (idx, term) in terms.iter().enumerate() {
328        let term_id = convert_term_to_graph(state, term);
329        add_edge(state, node_id.to_string(), term_id.clone(), format!("{prefix}{}", idx + 1));
330        last = Some(term_id);
331    }
332    last
333}
334
335// Ported from: JboTree.hs :: convertModal
336fn convert_modal(state: &mut GraphState, modal: &JboModalOp) -> (String, Option<String>, Option<String>, Option<String>) {
337    match modal {
338        JboModalOp::Tagged(tag, term) => {
339            let term_id = term.as_ref().map(|term| convert_term_to_graph(state, term));
340            ("tagged".to_string(), Some(crate::jbo_show::jboshow_tag(tag)), term_id, get_selmaho_from_tag(tag))
341        }
342        JboModalOp::WithEventAs(term) => {
343            let term_id = convert_term_to_graph(state, term);
344            ("event".to_string(), None, Some(term_id), Some("NOI".to_string()))
345        }
346        JboModalOp::QTruthModal => ("question".to_string(), Some("truth".to_string()), None, Some("UI".to_string())),
347        JboModalOp::NonVeridical => ("veridical".to_string(), Some("non-veridical".to_string()), None, Some("LE".to_string())),
348    }
349}
350
351// Ported from: JboTree.hs :: convertRel
352fn convert_rel(rel: &JboRel) -> (String, String, Option<String>) {
353    match rel {
354        JboRel::Brivla(s) => (s.clone(), "brivla".to_string(), Some("BRIVLA".to_string())),
355        JboRel::Equal => ("=".to_string(), "equal".to_string(), None),
356        JboRel::Among(_) => ("among".to_string(), "among".to_string(), None),
357        JboRel::Tanru(_, _) | JboRel::AppliedRel(_, _) => ("tanru".to_string(), "tanru".to_string(), Some("BRIVLA".to_string())),
358        JboRel::TanruConnective(_, _, _) => ("tanru-connective".to_string(), "tanru".to_string(), Some("JOI".to_string())),
359        JboRel::PermutedRel(_, inner) => convert_rel(inner),
360        JboRel::ScalarNegatedRel(_, inner) => {
361            let (name, rel_type, selmaho) = convert_rel(inner);
362            (format!("not {name}"), rel_type, selmaho)
363        }
364        JboRel::RVar(n) => (format!("R_{n}"), "var".to_string(), Some("KOhA".to_string())),
365        JboRel::BoundRVar(n) => (format!("R_{n}"), "bound-var".to_string(), Some("KOhA".to_string())),
366        JboRel::RAss(n) => (format!("R_{n}"), "assigned-var".to_string(), Some("KOhA".to_string())),
367        JboRel::UnboundBribasti(_) => ("unbound".to_string(), "unbound".to_string(), None),
368        JboRel::Moi(_, _) => ("moi".to_string(), "moi".to_string(), Some("MOI".to_string())),
369        JboRel::OperatorRel(_) => ("operator".to_string(), "operator".to_string(), None),
370        JboRel::VPredRel(_) => ("vpred".to_string(), "vpred".to_string(), None),
371        JboRel::AbsPred(_, _) => ("abstraction".to_string(), "abstraction".to_string(), Some("NU".to_string())),
372        JboRel::AbsProp(_, _) => ("abstraction-prop".to_string(), "abstraction".to_string(), Some("NU".to_string())),
373        JboRel::TagRel(tag) => ("tag".to_string(), "tag".to_string(), get_selmaho_from_tag(tag)),
374        JboRel::ModalRel(_, inner) => convert_rel(inner),
375    }
376}
377
378// Ported from: JboTree.hs :: getSelmahoFromTag
379fn get_selmaho_from_tag(tag: &JboTag) -> Option<String> {
380    match tag {
381        JboTag::DecoratedTagUnits(units) => units.iter().find_map(get_selmaho_from_dtu),
382        JboTag::ConnectedTag(_, t1, t2) => get_selmaho_from_tag(t1).or_else(|| get_selmaho_from_tag(t2)),
383    }
384}
385
386// Ported from: JboTree.hs :: getSelmahoFromDTU
387fn get_selmaho_from_dtu(dtu: &DecoratedTagUnit) -> Option<String> {
388    get_selmaho_from_tag_unit(&dtu.tag_unit)
389}
390
391// Ported from: JboTree.hs :: getSelmahoFromTagUnit
392fn get_selmaho_from_tag_unit(unit: &JboTagUnit) -> Option<String> {
393    match unit {
394        JboTagUnit::TenseCmavo(c) => lookup_selmaho(c),
395        JboTagUnit::CAhA(_) => Some("CAhA".to_string()),
396        JboTagUnit::FAhA(_, _) => Some("FAhA".to_string()),
397        JboTagUnit::ROI(_, _, _) => Some("ROI".to_string()),
398        JboTagUnit::TAhE_ZAhO(_, c) => lookup_selmaho(c),
399        JboTagUnit::BAI(_) => Some("BAI".to_string()),
400        JboTagUnit::FIhO(_) => Some("FIhO".to_string()),
401        JboTagUnit::CUhE(_) => Some("CUhE".to_string()),
402        JboTagUnit::KI => Some("KI".to_string()),
403    }
404}
405
406// Ported from: JboTree.hs :: lookupSelmaho
407fn lookup_selmaho(cmavo: &str) -> Option<String> {
408    let selmaho = if ["pu", "ca", "ba"].contains(&cmavo) {
409        "PU"
410    } else if ["zi", "za", "zu"].contains(&cmavo) {
411        "ZI"
412    } else if ["ze'i", "ze'a", "ze'u"].contains(&cmavo) {
413        "ZEhA"
414    } else if ["vi", "va", "vu"].contains(&cmavo) {
415        "VA"
416    } else if ["ve'i", "ve'a", "ve'u"].contains(&cmavo) {
417        "VEhA"
418    } else if ["vi'i", "vi'a", "vi'u"].contains(&cmavo) {
419        "VIhA"
420    } else if ["fa", "fe", "fi", "fo", "fu"].contains(&cmavo) {
421        "FA"
422    } else if ["co'i", "co'a", "co'u", "mo'u", "za'o", "de'a", "di'a"].contains(&cmavo) {
423        "ZAhO"
424    } else {
425        return None;
426    };
427    Some(selmaho.to_string())
428}
429
430// Ported from: JboTree.hs :: convertTermToGraph
431fn convert_term_to_graph(state: &mut GraphState, term: &JboTerm) -> String {
432    let default_key = format!("term:{}", content_hash(&format!("{:?}", term)));
433    match term {
434        JboTerm::JoikedTerms(joik, t1, t2) => {
435            let node_id = get_or_create_node(
436                state,
437                &default_key,
438                "term",
439                NodeData::Term { value: format!("joiked:{joik}"), term_type: "joiked".to_string(), selmaho: Some("JOI".to_string()) },
440            );
441            let t1_id = convert_term_to_graph(state, t1);
442            let t2_id = convert_term_to_graph(state, t2);
443            add_edge(state, node_id.clone(), t1_id, "t1".to_string());
444            add_edge(state, node_id.clone(), t2_id, "t2".to_string());
445            node_id
446        }
447        JboTerm::QualifiedTerm(_, term) => {
448            let node_id = get_or_create_node(
449                state,
450                &default_key,
451                "term",
452                NodeData::Term { value: "qualified".to_string(), term_type: "qualified".to_string(), selmaho: Some("LAhE".to_string()) },
453            );
454            let term_id = convert_term_to_graph(state, term);
455            add_edge(state, node_id.clone(), term_id, "term".to_string());
456            node_id
457        }
458        JboTerm::Constant(_, args) => {
459            let (value, term_type, _, selmaho) = convert_term(term);
460            let node_id = get_or_create_node(state, &default_key, "term", NodeData::Term { value, term_type, selmaho });
461            add_term_edges(state, &node_id, args, "arg");
462            node_id
463        }
464        JboTerm::JboQuote(_) => {
465            let key = format!("term:quote:{}", content_hash(&format!("{:?}", term)));
466            let (value, term_type, _, selmaho) = convert_term(term);
467            get_or_create_node(state, &key, "term", NodeData::Term { value, term_type, selmaho })
468        }
469        JboTerm::JboNonJboQuote(s) => {
470            let key = format!("term:quote:{}", content_hash(s));
471            let (value, term_type, _, selmaho) = convert_term(term);
472            get_or_create_node(state, &key, "term", NodeData::Term { value, term_type, selmaho })
473        }
474        JboTerm::TermWithSides(term, sides) => {
475            let term_id = convert_term_to_graph(state, term);
476            for side in sides {
477                if let Texticule::TexticuleSide(side_type, texticule) = side {
478                    let side_id = side_texticule_to_node(state, side_type, texticule);
479                    add_edge(state, term_id.clone(), side_id, "side".to_string());
480                }
481            }
482            term_id
483        }
484        _ => {
485            let (value, term_type, key, selmaho) = convert_term(term);
486            get_or_create_node(state, &key, "term", NodeData::Term { value, term_type, selmaho })
487        }
488    }
489}
490
491// Ported from: JboTree.hs :: convertTerm
492fn convert_term(term: &JboTerm) -> (String, String, String, Option<String>) {
493    match term {
494        JboTerm::BoundVar(n) => {
495            let value = format!("x_{n}");
496            (value.clone(), "var".to_string(), format!("term:var:{value}"), Some("KOhA".to_string()))
497        }
498        JboTerm::Var(n) => {
499            let value = format!("v_{n}");
500            (value.clone(), "var".to_string(), format!("term:var:{value}"), Some("KOhA".to_string()))
501        }
502        JboTerm::Constant(n, _) => {
503            let value = format!("c{n}");
504            (value.clone(), "constant".to_string(), format!("term:constant:{value}"), Some("KOhA".to_string()))
505        }
506        JboTerm::Named(s) | JboTerm::NonAnaph(s) => (s.clone(), "named".to_string(), format!("term:named:{s}"), Some("KOhA".to_string())),
507        JboTerm::PredNamed(_) => ("pred-named".to_string(), "complex".to_string(), "term:complex:pred-named".to_string(), None),
508        JboTerm::UnboundSumbasti(_) => ("unbound-sumbasti".to_string(), "complex".to_string(), "term:complex:unbound-sumbasti".to_string(), None),
509        JboTerm::JboQuote(_) => ("quote".to_string(), "quote".to_string(), "term:quote:quote".to_string(), Some("LU".to_string())),
510        JboTerm::JboErrorQuote(_) => ("error-quote".to_string(), "quote".to_string(), "term:quote:error-quote".to_string(), Some("LU".to_string())),
511        JboTerm::JboNonJboQuote(s) => (s.clone(), "quote".to_string(), format!("term:quote:{s}"), Some("ZO".to_string())),
512        JboTerm::TheMex(_) => ("mex".to_string(), "complex".to_string(), "term:complex:mex".to_string(), None),
513        JboTerm::Value(_) => ("value".to_string(), "value".to_string(), "term:value:value".to_string(), Some("LI".to_string())),
514        JboTerm::Valsi(s) => (s.clone(), "value".to_string(), format!("term:value:{s}"), Some("BRIVLA".to_string())),
515        JboTerm::Unfilled => ("unfilled".to_string(), "unfilled".to_string(), "term:unfilled".to_string(), None),
516        JboTerm::JoikedTerms(joik, _, _) => (format!("joiked:{joik}"), "joiked".to_string(), format!("term:joiked:{joik}"), Some("JOI".to_string())),
517        JboTerm::QualifiedTerm(_, _) => ("qualified".to_string(), "qualified".to_string(), "term:qualified".to_string(), Some("LAhE".to_string())),
518        JboTerm::TermWithSides(term, _) => convert_term(term),
519    }
520}
521
522// Ported from: JboTree.hs :: sideTexticuleToNode
523fn side_texticule_to_node(state: &mut GraphState, side_type: &crate::jbo_prop::SideType, texticule: &Texticule) -> String {
524    let side_type = format!("{:?}", side_type);
525    let content = match texticule {
526        Texticule::TexticuleProp(prop) => {
527            let prop_id = convert_prop_to_graph(state, prop, None);
528            format!("prop:{prop_id}")
529        }
530        Texticule::TexticuleSide(_, _) => "side".to_string(),
531        Texticule::TexticuleFrag(frag) => format!("{:?}", frag),
532    };
533    get_or_create_node(
534        state,
535        &format!("side:{side_type}:{content}"),
536        "side-texticule",
537        NodeData::SideTexticule { side_type, content },
538    )
539}
540
541// Ported from: JboTree.hs :: convertQuantifier
542fn convert_quantifier(q: &JboQuantifier) -> String {
543    match q {
544        JboQuantifier::MexQuantifier(mex) => format!("{:?}", mex),
545        JboQuantifier::LojQuantifier(q) => q.to_string(),
546        JboQuantifier::QuestionQuantifier => "?".to_string(),
547        JboQuantifier::RelQuantifier(q) => format!("R({})", convert_quantifier(q)),
548    }
549}
550
551// Ported from: JboTree.hs :: jboPropToGraph
552pub fn jbo_prop_to_graph(prop: &JboProp) -> GraphOutput {
553    let mut state = GraphState::new();
554    convert_prop_to_graph(&mut state, prop, None);
555    GraphOutput { format: "graph".to_string(), nodes: state.nodes, edges: state.edges }
556}
557
558// Ported from: JboTree.hs :: jboPropsToGraph
559pub fn jbo_props_to_graph(props: &[JboProp]) -> GraphOutput {
560    let mut state = GraphState::new();
561    for prop in props {
562        convert_prop_to_graph(&mut state, prop, None);
563    }
564    GraphOutput { format: "graph".to_string(), nodes: state.nodes, edges: state.edges }
565}