1use 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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct GraphEdge {
84 pub source: String,
85 pub target: String,
86 pub label: String,
87}
88
89struct 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
108fn 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
117fn content_hash(s: &str) -> String {
119 format!("h{}", simple_hash(s).abs())
120}
121
122fn 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
146fn add_edge(state: &mut GraphState, source: String, target: String, label: String) {
148 state.edges.push(GraphEdge { source, target, label });
149}
150
151fn 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
253fn 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
258fn 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
265fn 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
324fn 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
335fn 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
351fn 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
378fn 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
386fn get_selmaho_from_dtu(dtu: &DecoratedTagUnit) -> Option<String> {
388 get_selmaho_from_tag_unit(&dtu.tag_unit)
389}
390
391fn 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
406fn 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
430fn 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
491fn 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
522fn 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
541fn 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
551pub 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
558pub 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}